-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add code to convert a TNM stage to a cancer stage
- Loading branch information
1 parent
091ad13
commit 3f5b9d8
Showing
3 changed files
with
233 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Converting TNM to a Staging Value | ||
|
||
The current algorithm for determining the numeric staging algorithm is based on | ||
the [TNM classification](https://www.ncbi.nlm.nih.gov/books/NBK553187/) and | ||
works as follows: | ||
|
||
1. If M is 1, staging is 4. Skip remaining steps. | ||
2. If N is greater than 0, staging is 3. Skip remaining steps. | ||
3. If T is greater than 2, staging is 2. Skip remaining steps. | ||
4. If T is 1, staging is 1. Skip remaining steps. | ||
5. If T is "is" (in situ), staging is 0. Skip remaining steps. | ||
6. Otherwise, the value is T0 N0 M0 which is effectively "no cancer." This has no corresponding staging, so staging is `null`. | ||
|
||
This breaks down as follows: | ||
|
||
| T | N | M | Staging | | ||
|--------|------------|----|---------| | ||
| T0 | N0 | M0 | `null` | | ||
| Tis | N0 | M0 | 0 | | ||
| T1, T2 | N0 | M0 | 1 | | ||
| T3, T4 | N0 | M0 | 2 | | ||
| (any) | N1, N2, N3 | M0 | 3 | | ||
| (any) | (any) | M1 | 4 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { parseTNM, convertTNMToCancerStage, convertTNMValuesToCancerStage } from '../src/tnm'; | ||
|
||
describe('parseTNM()', () => { | ||
it('parses a simple set of fields', () => { | ||
expect(parseTNM('T1 N0 M0')).toEqual([ | ||
{ | ||
parameter: 'T', | ||
value: 1, | ||
prefixModifiers: '', | ||
suffixModifiers: '' | ||
}, | ||
{ | ||
parameter: 'N', | ||
value: 0, | ||
prefixModifiers: '', | ||
suffixModifiers: '' | ||
}, | ||
{ | ||
parameter: 'M', | ||
value: 0, | ||
prefixModifiers: '', | ||
suffixModifiers: '' | ||
} | ||
]); | ||
}); | ||
it('parses "Tis"', () => { | ||
expect(parseTNM('Tis')).toEqual([ | ||
{ | ||
parameter: 'T', | ||
value: null, | ||
prefixModifiers: '', | ||
suffixModifiers: 'is' | ||
} | ||
]); | ||
}); | ||
it('handles junk', () => { | ||
expect(parseTNM('this is invalid')).toEqual([]); | ||
}); | ||
}); | ||
|
||
describe('convertTNMToCancerStage()', () => { | ||
it('returns undefined for invalid values', () => { | ||
expect(convertTNMToCancerStage('completely invalid')).toBe(undefined); | ||
}); | ||
it('returns undefined when fields are missing', () => { | ||
expect(convertTNMToCancerStage('missing data T1')).toBe(undefined); | ||
expect(convertTNMToCancerStage('missing data T1 N0')).toBe(undefined); | ||
expect(convertTNMToCancerStage('missing data T1 M0')).toBe(undefined); | ||
expect(convertTNMToCancerStage('missing data N0')).toBe(undefined); | ||
expect(convertTNMToCancerStage('missing data N0 M0')).toBe(undefined); | ||
expect(convertTNMToCancerStage('missing data M0')).toBe(undefined); | ||
expect(convertTNMToCancerStage('This alMost looks valid but is Not')).toBe(undefined); | ||
}); | ||
it('returns null for T0 N0 M0', () => { | ||
expect(convertTNMToCancerStage('T0 N0 M0')).toBe(null); | ||
}); | ||
it('returns 0 for Tis N0 M0', () => { | ||
expect(convertTNMToCancerStage('Tis N0 M0')).toEqual(0); | ||
expect(convertTNMToCancerStage('Tis N0 M0 with added junk')).toEqual(0); | ||
}); | ||
it('returns as expected even with extra field', () => { | ||
expect(convertTNMToCancerStage('pT1 pN0 M0 R0 G1')).toEqual(1); | ||
}); | ||
}); | ||
|
||
describe('convertTNMValuesToCancerStage()', () => { | ||
it('handles T0 N0 M0', () => { | ||
expect(convertTNMValuesToCancerStage(0, 0, 0)).toBeNull(); | ||
}); | ||
it('converts Tis N0 M0 to 0', () => { | ||
expect(convertTNMValuesToCancerStage(0.5, 0, 0)).toEqual(0); | ||
}); | ||
it('converts T1/T2 N0 M0 to 1', () => { | ||
expect(convertTNMValuesToCancerStage(1, 0, 0)).toEqual(1); | ||
expect(convertTNMValuesToCancerStage(2, 0, 0)).toEqual(1); | ||
}); | ||
it('converts T3/T4 N0 M0 to 2', () => { | ||
expect(convertTNMValuesToCancerStage(3, 0, 0)).toEqual(2); | ||
expect(convertTNMValuesToCancerStage(4, 0, 0)).toEqual(2); | ||
}); | ||
it('converts T? N1/N2/N3 M0 to 3', () => { | ||
for (let t = 0; t <= 4; t++) { | ||
expect(convertTNMValuesToCancerStage(t, 1, 0)).toEqual(3); | ||
expect(convertTNMValuesToCancerStage(t, 2, 0)).toEqual(3); | ||
expect(convertTNMValuesToCancerStage(t, 3, 0)).toEqual(3); | ||
} | ||
}); | ||
it('converts T? N? M1 to 4', () => { | ||
for (let t = 1; t <= 4; t++) { | ||
for (let n = 0; n <= 4; n++) { | ||
expect(convertTNMValuesToCancerStage(t, n, 1)).toEqual(4); | ||
} | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
export type CancerStage = 0 | 1 | 2 | 3 | 4; | ||
|
||
export interface TNMField { | ||
/** | ||
* Upper case parameter field | ||
*/ | ||
parameter: string; | ||
/** | ||
* The value after this field, *may be null* if there is no number (e.g., Tis) | ||
*/ | ||
value: number | null; | ||
/** | ||
* A set of lowercase letters the modify the code that came before the field. | ||
*/ | ||
prefixModifiers: string; | ||
/** | ||
* An optional set of suffix modifiers that came after the field. | ||
*/ | ||
suffixModifiers: string; | ||
} | ||
|
||
/** | ||
* Parses a given string into TNM fields. These fields should be space delimited (e.g., "T1 N0 M0"). If nothing can | ||
* be pulled out of the string, an empty array may be returned. | ||
* @param str the string to parse | ||
*/ | ||
export function parseTNM(str: string): TNMField[] { | ||
// First, split by whitespace | ||
const fields = str.split(/\s+/); | ||
// Then parse the individual fields | ||
return fields | ||
.map<TNMField | null>((field) => { | ||
const m = /^([a-z]*)([A-Z])(\d?)([a-z]*)$/.exec(field); | ||
if (m) { | ||
return { | ||
parameter: m[2], | ||
value: m[3] ? parseInt(m[3]) : null, | ||
prefixModifiers: m[1], | ||
suffixModifiers: m[4] | ||
}; | ||
} else { | ||
return null; | ||
} | ||
}) | ||
.filter((value): value is TNMField => value !== null); | ||
} | ||
|
||
/** | ||
* Converts a set of TNM values into cancer stages. | ||
* @param tnm a space-separated set of TNM values | ||
* @returns the converted cancer stage, or null if no cancer was given, or | ||
* undefined if the TNM values couldn't be parsed | ||
*/ | ||
export function convertTNMToCancerStage(tnm: string): CancerStage | null | undefined { | ||
// First, attempt to parse this at all | ||
let t: number | undefined = undefined; | ||
let n: number | undefined = undefined; | ||
let m: number | undefined = undefined; | ||
for (const field of parseTNM(tnm)) { | ||
switch (field.parameter) { | ||
case 'T': | ||
if (field.value !== null) { | ||
t = field.value; | ||
} else if (field.suffixModifiers === 'is') { | ||
t = 0.5; | ||
} | ||
break; | ||
case 'N': | ||
if (field.value !== null) { | ||
n = field.value; | ||
} | ||
break; | ||
case 'M': | ||
if (field.value !== null) { | ||
m = field.value; | ||
} | ||
break; | ||
default: | ||
// Ignore this field | ||
} | ||
} | ||
if (typeof t === 'number' && typeof n === 'number' && typeof m === 'number') { | ||
return convertTNMValuesToCancerStage(t, n, m); | ||
} else { | ||
return undefined; | ||
} | ||
} | ||
|
||
/** | ||
* Converts a given set of TNM numbers to a corresponding cancer stage. | ||
* @param tumor the tumor number (0-4, with 0.5 being used for Tis) | ||
* @param node the node number | ||
* @param metastasis the metastasis number | ||
* @returns a corresponding cancer stage number | ||
*/ | ||
export function convertTNMValuesToCancerStage(tumor: number, node: number, metastasis: number): CancerStage | null { | ||
if (metastasis > 0) { | ||
return 4; | ||
} | ||
if (node > 0) { | ||
return 3; | ||
} | ||
if (tumor > 2) { | ||
return 2; | ||
} | ||
if (tumor >= 1) { | ||
return 1; | ||
} | ||
if (tumor === 0) { | ||
// This is T0 N0 M0 which means "no tumor found" which doesn't really make | ||
// sense | ||
return null; | ||
} | ||
return 0; | ||
} |