@@ -2,18 +2,44 @@ import { computed, Injectable, signal } from '@angular/core';
22import { AbstractControl , ValidationErrors , ValidatorFn } from '@angular/forms' ;
33
44
5+ // regexes for human strings such as 4y2m and 4yrs 2nth
6+ const RE_YEAR = / ( \d + (?: \. \d + ) ? ) \s * y / i;
7+ const RE_MONTH = / ( \d + (?: \. \d + ) ? ) \s * m / i;
8+ const RE_WEEK = / ( \d + (?: \. \d + ) ? ) \s * w / i;
9+ const RE_DAY = / ( \d + ) \s * d / i;
10+ // Matches ISO 8601 durations like "P1Y6M3D" or "P0D"
11+ const ISO8601_RE = / ^ P (?: \d + Y ) ? (?: \d + M ) ? (?: \d + D ) ? $ / i;
12+ const GESTATIONAL_AGE_RE = / ^ G ( \d + ) w (?: ( [ 0 - 6 ] ) d ) ? $ / i;
13+
14+
515@Injectable ( {
616 providedIn : 'root'
717} )
818export class AgeInputService {
9- readonly onsetTerms = [ 'na' , 'Antenatal onset' ,
19+ readonly ALLOWED_AGE_LABELS = new Set ( [ 'na' , 'Antenatal onset' ,
1020 'Embryonal onset' , 'Fetal onset' ,
1121 'Late first trimester onset' , 'Second trimester onset' , 'Third trimester onset' ,
1222 'Congenital onset' ,
1323 'Pediatric onset' ,
1424 'Neonatal onset' , 'Infantile onset' , 'Childhood onset' , 'Juvenile onset' ,
1525 'Adult onset' , 'Young adult onset' , 'Early young adult onset' , 'Intermediate young adult onset' , 'Late young adult onset' ,
16- 'Middle age onset' , 'Late onset' ] ;
26+ 'Middle age onset' , 'Late onset' ] ) ;
27+
28+ readonly AGE_TERM_MAP : Record < string , string > = {
29+ antenatal : "Antenatal onset" ,
30+ neonate : "Neonatal onset" ,
31+ neonatal : "Neonatal onset" ,
32+ birth : "Congenital onset" ,
33+ congenital : "Congenital onset" ,
34+ childhood : "Childhood onset" ,
35+ adult : "Adult onset" ,
36+ unk : "na" ,
37+ na : "na" ,
38+ } ;
39+
40+
41+
42+
1743
1844 readonly isoPattern = / ^ P (?: \d + Y ) ? (?: \d + M ) ? (?: \d + D ) ? $ / ;
1945 readonly gestationalAgePattern = / ^ G \d { 1 , 2 } w (?: [ 0 - 6 ] d ) ? $ / ;
@@ -24,14 +50,14 @@ export class AgeInputService {
2450 readonly selectedTerms = this . _selectedTerms . asReadonly ( ) ;
2551 /** for autocomplete lists */
2652 readonly allAvailableTerms = computed ( ( ) => {
27- return Array . from ( new Set ( [ ...this . onsetTerms , ...this . _selectedTerms ( ) ] ) ) ;
53+ return Array . from ( new Set ( [ ...this . ALLOWED_AGE_LABELS , ...this . _selectedTerms ( ) ] ) ) ;
2854 } ) ;
2955 /**
3056 * Returns true if the input is a valid ISO8601 age string, a gestational age string, or a known HPO term ("na" is also an allowed entry)
3157 */
3258 validateAgeInput ( input : string ) : boolean {
3359 return input == "na" ||
34- this . onsetTerms . includes ( input ) ||
60+ this . ALLOWED_AGE_LABELS . has ( input ) ||
3561 this . isoPattern . test ( input ) ||
3662 this . gestationalAgePattern . test ( input ) ;
3763 }
@@ -67,4 +93,50 @@ export class AgeInputService {
6793 this . _selectedTerms . set ( [ "na" ] ) ;
6894 }
6995
96+ mapAgeStringToSymbolic ( input : string ) : string | null {
97+ const lower = input . toLowerCase ( ) ;
98+ if ( this . AGE_TERM_MAP [ lower ] ) return this . AGE_TERM_MAP [ lower ] ;
99+ if ( this . ALLOWED_AGE_LABELS . has ( input ) ) return input ;
100+ return null ;
101+ }
102+
103+ mapYmdToIso ( input : string ) : string | undefined {
104+ const yMatch = RE_YEAR . exec ( input ) ;
105+ const mMatch = RE_MONTH . exec ( input ) ;
106+ const wMatch = RE_WEEK . exec ( input ) ;
107+ const dMatch = RE_DAY . exec ( input ) ;
108+
109+ const yVal = yMatch ? parseFloat ( yMatch [ 1 ] ) : 0 ;
110+ const mVal = mMatch ? parseFloat ( mMatch [ 1 ] ) : 0 ;
111+ const wVal = wMatch ? parseFloat ( wMatch [ 1 ] ) : 0 ;
112+ const dVal = dMatch ? parseFloat ( dMatch [ 1 ] ) : 0 ;
113+
114+ console . log ( `input=${ input } y=${ yVal } m=${ mVal } w=${ wVal } d=${ dVal } ` ) ;
115+
116+ if ( yVal === 0 && mVal === 0 && wVal === 0 && dVal === 0 ) return undefined ;
117+
118+ const years = Math . floor ( yVal ) ;
119+ const monthsFromY = Math . round ( ( yVal - years ) * 12 ) ;
120+ const totalMonths = Math . floor ( mVal ) + monthsFromY ;
121+
122+ const daysFromW = Math . round ( wVal * 7 ) ;
123+ const totalDays = Math . floor ( dVal ) + daysFromW ;
124+
125+ let res = "P" ;
126+ if ( years > 0 ) res += `${ years } Y` ;
127+ if ( totalMonths > 0 ) res += `${ totalMonths } M` ;
128+ if ( totalDays > 0 ) res += `${ totalDays } D` ;
129+
130+ return res === "P" ? undefined : res ;
131+ }
132+
133+ mapEtlAgeString ( input : string | null | undefined ) : string | undefined {
134+ if ( ! input ) return undefined ;
135+ const symbolic = this . mapAgeStringToSymbolic ( input ) ;
136+ if ( symbolic ) return symbolic ;
137+ if ( GESTATIONAL_AGE_RE . test ( input ) ) return input ;
138+ if ( ISO8601_RE . test ( input ) ) return input ;
139+ return this . mapYmdToIso ( input ) ;
140+ }
141+
70142}
0 commit comments