@@ -221,7 +221,7 @@ const UnitXref = {
221
221
* @typedef {string } MeasurementKey - unique identifier for a measurement type, used as a key in Measurement.
222
222
*/
223
223
224
- /**
224
+ /**
225
225
* @typedef {Object } Measurement
226
226
* @property {MeasurementKey } key - the key of the measurement type
227
227
* @property {string } value - the value of the measurement type
@@ -245,8 +245,7 @@ const Measurement = {
245
245
* @property {MeasurementKey } type - the type of measurement this unit belongs to, one of the keys in `Measurement`.
246
246
* @property {string } label - the label for the unit, used in dropdown options and axis labels. If undefined, the key
247
247
* is used as the label.
248
- * @property {Array<string> } aliases - an object containing aliases for the unit, used for case-insensitive matching. If
249
- * undefined, only the key (and label) is used for matching.
248
+ * @property {Array<string> } aliases - list of aliases for the unit, used for matching besides the key and label.
250
249
*/
251
250
252
251
/**
@@ -271,15 +270,15 @@ const UnitMetadata = {
271
270
A : {
272
271
type : Measurement . LAMBDA . key ,
273
272
label : WAVELENGTH_UNITS . angstrom . symbol ,
274
- aliases : [ 'angstrom ' , 'angstroms' ] //case-insensitive
273
+ aliases : [ 'Angstrom ' , 'angstrom' ]
275
274
} ,
276
275
nm : {
277
276
type : Measurement . LAMBDA . key ,
278
277
} ,
279
278
um : {
280
279
type : Measurement . LAMBDA . key ,
281
280
label : WAVELENGTH_UNITS . um . symbol ,
282
- aliases : [ 'micron' , 'microns' ] //case-insensitive
281
+ aliases : [ 'micron' , 'microns' ]
283
282
} ,
284
283
mm : {
285
284
type : Measurement . LAMBDA . key ,
@@ -294,7 +293,6 @@ const UnitMetadata = {
294
293
'W/m^2/Hz' : {
295
294
type : Measurement . F_NU . key ,
296
295
label : 'W/m²/Hz' ,
297
- aliases : [ ] , //TODO: generate aliases in dot product notation with **, as it is with ** for powers
298
296
} ,
299
297
'erg/s/cm^2/Hz' : {
300
298
type : Measurement . F_NU . key ,
@@ -329,7 +327,7 @@ const UnitMetadata = {
329
327
330
328
331
329
/**
332
- * Maps any unit value/label/alias back to a key in UnitXref (and UnitMetadata).
330
+ * Maps any unit representation ( value/label/alias) back to a key in UnitXref (and UnitMetadata).
333
331
* @param u {string} - the unit to normalize
334
332
* @return {UnitKey|null } - the key in UnitXref if found, otherwise null
335
333
*/
@@ -339,11 +337,80 @@ function normalizeUnit(u) {
339
337
if ( UnitXref [ u ] ) return u ;
340
338
341
339
for ( const [ key , meta ] of Object . entries ( UnitMetadata ) ) {
342
- // u is a case-insensitive alias of a key in UnitXref
343
- if ( meta ?. aliases ?. some ( ( alias ) => alias . toLowerCase ( ) === u . toLowerCase ( ) ) ) return key ;
340
+ // u is an alias of a key in UnitXref
341
+ const aliases = meta ?. aliases ?? [ ] ;
342
+ aliases . push ( ...getFluxAliases ( key ) ) ;
343
+ if ( aliases . some ( ( alias ) => alias === u ) ) return key ;
344
344
345
345
// u is a label of a key in UnitXref
346
346
if ( meta ?. label === u ) return key ;
347
347
}
348
348
return null ;
349
349
}
350
+
351
+ /* Get all possible representations (key, label, aliases) for a given unitKey */
352
+ function getAllRepresentations ( unitKey , includeLabel = true ) {
353
+ const meta = UnitMetadata [ unitKey ] ;
354
+ if ( ! meta ) return [ ] ; //invalid unitKey
355
+ const reps = [ unitKey ] ; // include the key itself
356
+ if ( includeLabel && meta . label && meta . label !== unitKey ) reps . push ( meta . label ) ;
357
+ if ( Array . isArray ( meta . aliases ) ) reps . push ( ...meta . aliases ) ;
358
+ return reps ;
359
+ }
360
+
361
+ /**
362
+ * Decomposes a unit string into its base units and their powers.
363
+ * @param unit {string} - the unit string to decompose, e.g. 'erg/s/cm^2/A'
364
+ * @returns {Map<any, any> } - a Map where keys are base units and values are their powers, e.g.
365
+ * `Map{'erg'=> 1, 's'=> -1, 'cm'=> -2, 'A'=> -1}`
366
+ */
367
+ function decomposeUnit ( unit ) {
368
+ const pattern = / ( [ * . / ] ? ) ( [ A - Z a - z ] + ) (?: \^ ( [ + - ] ? \d + ) ) ? / g;
369
+ const result = new Map ( ) ; // to preserve the order of units
370
+ const matches = unit . matchAll ( pattern ) ;
371
+ for ( const match of matches ) {
372
+ const [ , operator , baseUnit , exponent ] = match ;
373
+ let power = exponent ? Number ( exponent ) : 1 ;
374
+ if ( operator === '/' ) power = - power ; // '/' implies negative exponent, '*' or '.' implies positive exponent
375
+ const prevPower = result . get ( baseUnit ) || 0 ;
376
+ result . set ( baseUnit , prevPower + power ) ;
377
+ }
378
+ return result ;
379
+ }
380
+
381
+ /**
382
+ * Get all possible aliases for a flux unit, this includes permutations of its expression notations and its consisting
383
+ * units' aliases.
384
+ * @param unitKey {UnitKey}
385
+ * @returns {Array<string> }
386
+ */
387
+ const getFluxAliases = ( unitKey ) => {
388
+ if ( ! UnitMetadata [ unitKey ] ?. type . startsWith ( 'F' ) ) return [ ] ; //not a flux unit
389
+
390
+ let fluxRepresentations ;
391
+ if ( UnitMetadata [ unitKey ] ?. type . startsWith ( 'F_' ) ) { //flux density (in nu or lambda)
392
+ const nuOrLambdaUnitKey = unitKey . substring ( unitKey . lastIndexOf ( '/' ) + 1 ) ; // extract the last part after '/'
393
+ // nu or lambda's representations propagate in the flux density representations
394
+ // e.g. erg/s/cm^2/A -> erg/s/cm^2/Angstrom, erg/s/cm^2/angstrom, because of the 'A' unit's alternative representations
395
+ fluxRepresentations = getAllRepresentations ( nuOrLambdaUnitKey , false ) . map (
396
+ ( rep ) => unitKey . replace ( nuOrLambdaUnitKey , rep ) ) ;
397
+ }
398
+ else {
399
+ fluxRepresentations = [ unitKey ] ; // inband flux remains the same since it is independent of nu or lambda
400
+ }
401
+
402
+ const aliases = [ ] ;
403
+ for ( const fluxUnit of fluxRepresentations ) { // generate all possible expression notations for the flux unit
404
+ const asteriskPowerExpr = fluxUnit . replaceAll ( '^' , '**' ) ; // e.g. erg/s/cm**2/A
405
+ const invisiblePowerExpr = fluxUnit . replaceAll ( '^' , '' ) ; // e.g. erg/s/cm2/A
406
+
407
+ const multiplicationExpr = Array . from ( decomposeUnit ( fluxUnit ) . entries ( ) )
408
+ . map ( ( [ unit , power ] ) => power === 1 ? unit : `${ unit } **${ power } ` )
409
+ . join ( '.' ) ; // e.g. erg.s**-1.cm**-2.A**-1
410
+
411
+ // TODO: also support numerical scale-factor in section 2.10 in https://ivoa.net/documents/VOUnits/20231215/REC-VOUnits-1.1.pdf
412
+ // possibly alias function can return a boolean to do such matching
413
+ aliases . push ( fluxUnit , asteriskPowerExpr , invisiblePowerExpr , multiplicationExpr ) ;
414
+ }
415
+ return Array . from ( new Set ( aliases ) ) ; // remove duplicates
416
+ } ;
0 commit comments