2021-08-07 23:11:34 +02:00
import BaseUIElement from "../UI/BaseUIElement"
import { Denomination } from "./Denomination"
2021-09-13 01:21:47 +02:00
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
2023-12-12 03:46:51 +01:00
import unit from "../../assets/layers/unit/unit.json"
2024-04-28 22:13:25 +02:00
import { QuestionableTagRenderingConfigJson } from "./ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import TagRenderingConfig from "./ThemeConfig/TagRenderingConfig"
import Validators , { ValidatorType } from "../UI/InputElement/Validators"
import { Validator } from "../UI/InputElement/Validator"
2021-08-07 23:11:34 +02:00
export class Unit {
2023-12-12 03:46:51 +01:00
private static allUnits = this . initUnits ( )
2021-08-07 23:11:34 +02:00
public readonly appliesToKeys : Set < string >
public readonly denominations : Denomination [ ]
public readonly denominationsSorted : Denomination [ ]
public readonly eraseInvalid : boolean
2023-12-12 03:46:51 +01:00
public readonly quantity : string
2024-04-28 22:13:25 +02:00
private readonly _validator : Validator
2024-04-28 23:04:09 +02:00
public readonly inverted : boolean
2022-09-08 21:40:48 +02:00
2022-08-18 19:17:15 +02:00
constructor (
2023-12-12 03:46:51 +01:00
quantity : string ,
2022-08-18 19:17:15 +02:00
appliesToKeys : string [ ] ,
applicableDenominations : Denomination [ ] ,
2024-04-28 22:13:25 +02:00
eraseInvalid : boolean ,
2024-04-28 23:04:09 +02:00
validator : Validator ,
inverted : boolean = false
2022-08-18 19:17:15 +02:00
) {
2023-12-12 03:46:51 +01:00
this . quantity = quantity
2024-04-28 22:13:25 +02:00
this . _validator = validator
2024-06-19 01:33:30 +02:00
if ( ! inverted && [ "string" , "text" , "key" , "icon" , "translation" , "fediverse" , "id" ] . indexOf ( validator . name ) >= 0 ) {
console . trace ( "Unit error" )
throw "Invalid unit configuration. The validator is of a forbidden type: " + validator . name + "; set a (number) type to your freeform key or set inverted. Hint: this unit is applied onto keys: " + appliesToKeys . join ( "; " )
}
2024-04-28 23:04:09 +02:00
this . inverted = inverted
2021-08-07 23:11:34 +02:00
this . appliesToKeys = new Set ( appliesToKeys )
2022-08-18 19:17:15 +02:00
this . denominations = applicableDenominations
2021-08-07 23:11:34 +02:00
this . eraseInvalid = eraseInvalid
const seenUnitExtensions = new Set < string > ( )
for ( const denomination of this . denominations ) {
if ( seenUnitExtensions . has ( denomination . canonical ) ) {
throw (
"This canonical unit is already defined in another denomination: " +
denomination . canonical
2022-09-08 21:40:48 +02:00
)
2021-08-07 23:11:34 +02:00
}
const duplicate = denomination . alternativeDenominations . filter ( ( denom ) = >
seenUnitExtensions . has ( denom )
2022-09-08 21:40:48 +02:00
)
2021-08-07 23:11:34 +02:00
if ( duplicate . length > 0 ) {
throw "A denomination is used multiple times: " + duplicate . join ( ", " )
}
seenUnitExtensions . add ( denomination . canonical )
denomination . alternativeDenominations . forEach ( ( d ) = > seenUnitExtensions . add ( d ) )
}
this . denominationsSorted = [ . . . this . denominations ]
this . denominationsSorted . sort ( ( a , b ) = > b . canonical . length - a . canonical . length )
const possiblePostFixes = new Set < string > ( )
function addPostfixesOf ( str ) {
2021-11-07 16:34:51 +01:00
if ( str === undefined ) {
return
}
2021-08-07 23:11:34 +02:00
str = str . toLowerCase ( )
for ( let i = 0 ; i < str . length + 1 ; i ++ ) {
const substr = str . substring ( 0 , i )
possiblePostFixes . add ( substr )
}
}
for ( const denomination of this . denominations ) {
addPostfixesOf ( denomination . canonical )
2021-09-13 02:38:20 +02:00
addPostfixesOf ( denomination . _canonicalSingular )
2021-08-07 23:11:34 +02:00
denomination . alternativeDenominations . forEach ( addPostfixesOf )
}
}
2023-12-12 03:46:51 +01:00
static fromJson (
json :
| UnitConfigJson
2024-06-16 16:06:26 +02:00
| Record <
string ,
string | { quantity : string ; denominations : string [ ] ; inverted? : boolean }
> ,
2024-04-28 22:13:25 +02:00
tagRenderings : TagRenderingConfig [ ] ,
2023-12-12 03:46:51 +01:00
ctx : string
) : Unit [ ] {
2024-04-28 22:13:25 +02:00
let types : Record < string , ValidatorType > = { }
for ( const tagRendering of tagRenderings ) {
if ( tagRendering . freeform ? . type ) {
types [ tagRendering . freeform . key ] = tagRendering . freeform . type
}
}
2023-12-12 03:46:51 +01:00
if ( ! json . appliesToKey && ! json . quantity ) {
2024-04-28 22:13:25 +02:00
return this . loadFromLibrary ( < any > json , types , ctx )
2023-12-12 03:46:51 +01:00
}
2024-04-28 22:13:25 +02:00
return this . parse ( < UnitConfigJson > json , types , ctx )
}
2024-06-16 16:06:26 +02:00
private static parseDenomination (
json : UnitConfigJson ,
validator : Validator ,
appliesToKey : string ,
ctx : string
) : Unit {
2024-04-28 22:13:25 +02:00
const applicable = json . applicableUnits . map ( ( u , i ) = >
Denomination . fromJson ( u , validator , ` ${ ctx } .units[ ${ i } ] ` )
)
if (
json . defaultInput &&
! applicable . some ( ( denom ) = > denom . canonical . trim ( ) === json . defaultInput )
) {
throw ` ${ ctx } : no denomination has the specified default denomination. The default denomination is ' ${
json . defaultInput
} ' , but the available denominations are $ { applicable
. map ( ( denom ) = > denom . canonical )
. join ( ", " ) } `
}
return new Unit (
json . quantity ? ? "" ,
appliesToKey === undefined ? undefined : [ appliesToKey ] ,
applicable ,
json . eraseInvalidValues ? ? false ,
validator
)
2023-12-12 03:46:51 +01:00
}
2023-01-02 02:35:40 +01:00
/ * *
*
* // Should detect invalid defaultInput
* let threwError = false
* try {
2023-12-12 03:46:51 +01:00
* Unit . parse ( {
2023-01-02 02:35:40 +01:00
* appliesToKey : [ "length" ] ,
* defaultInput : "xcm" ,
* applicableUnits : [
* {
* canonicalDenomination : "m" ,
* useIfNoUnitGiven : true ,
* human : "meter"
* }
* ]
* } , "test" )
* } catch ( e ) {
2023-01-03 00:36:44 +01:00
* threwError = true
2023-01-02 02:35:40 +01:00
* }
2023-01-03 00:36:44 +01:00
* threwError // => true
2023-01-02 02:35:40 +01:00
*
* // Should work
2023-12-12 03:46:51 +01:00
* Unit . parse ( {
2023-01-02 02:35:40 +01:00
* appliesToKey : [ "length" ] ,
* defaultInput : "xcm" ,
* applicableUnits : [
* {
* canonicalDenomination : "m" ,
* useIfNoUnitGiven : true ,
* humen : "meter"
* } ,
* {
* canonicalDenomination : "cm" ,
* human : "centimeter"
* }
* ]
* } , "test" )
* /
2024-06-16 16:06:26 +02:00
private static parse (
json : UnitConfigJson ,
types : Record < string , ValidatorType > ,
ctx : string
) : Unit [ ] {
2021-09-13 01:21:47 +02:00
const appliesTo = json . appliesToKey
2023-12-12 03:46:51 +01:00
for ( let i = 0 ; i < ( appliesTo ? ? [ ] ) . length ; i ++ ) {
2021-09-13 01:21:47 +02:00
let key = appliesTo [ i ]
if ( key . trim ( ) !== key ) {
throw ` ${ ctx } .appliesToKey[ ${ i } ] is invalid: it starts or ends with whitespace `
}
}
if ( ( json . applicableUnits ? ? [ ] ) . length === 0 ) {
throw ` ${ ctx } : define at least one applicable unit `
}
// Some keys do have unit handling
2024-04-28 22:13:25 +02:00
const units : Unit [ ] = [ ]
if ( appliesTo === undefined ) {
units . push ( this . parseDenomination ( json , Validators . get ( "float" ) , undefined , ctx ) )
2023-06-11 02:32:14 +02:00
}
2024-04-28 22:13:25 +02:00
for ( const key of appliesTo ? ? [ ] ) {
const validator = Validators . get ( types [ key ] ? ? "float" )
units . push ( this . parseDenomination ( json , validator , undefined , ctx ) )
}
return units
2023-12-12 03:46:51 +01:00
}
private static initUnits ( ) : Map < string , Unit > {
const m = new Map < string , Unit > ( )
2024-04-28 22:13:25 +02:00
const units = ( < UnitConfigJson [ ] > unit . units ) . flatMap ( ( json , i ) = >
this . parse ( json , { } , "unit.json.units." + i )
2023-12-12 03:46:51 +01:00
)
for ( const unit of units ) {
m . set ( unit . quantity , unit )
}
return m
}
private static getFromLibrary ( name : string , ctx : string ) : Unit {
const loaded = this . allUnits . get ( name )
if ( loaded === undefined ) {
throw (
"No unit with quantity name " +
name +
" found (at " +
ctx +
"). Try one of: " +
Array . from ( this . allUnits . keys ( ) ) . join ( ", " )
)
}
return loaded
}
private static loadFromLibrary (
spec : Record <
string ,
2024-06-16 16:06:26 +02:00
| string
| { quantity : string ; denominations : string [ ] ; canonical? : string ; inverted? : boolean }
2023-12-12 03:46:51 +01:00
> ,
2024-04-28 22:13:25 +02:00
types : Record < string , ValidatorType > ,
2023-12-12 03:46:51 +01:00
ctx : string
) : Unit [ ] {
const units : Unit [ ] = [ ]
for ( const key in spec ) {
const toLoad = spec [ key ]
2024-04-28 22:13:25 +02:00
const validator = Validators . get ( types [ key ] ? ? "float" )
2023-12-12 03:46:51 +01:00
if ( typeof toLoad === "string" ) {
const loaded = this . getFromLibrary ( toLoad , ctx )
units . push (
2024-06-16 16:06:26 +02:00
new Unit (
loaded . quantity ,
[ key ] ,
loaded . denominations ,
loaded . eraseInvalid ,
validator ,
toLoad [ "inverted" ]
)
2023-12-12 03:46:51 +01:00
)
continue
}
const loaded = this . getFromLibrary ( toLoad . quantity , ctx )
const quantity = toLoad . quantity
2024-01-22 03:42:00 +01:00
2023-12-12 03:46:51 +01:00
function fetchDenom ( d : string ) : Denomination {
const found = loaded . denominations . find (
( denom ) = > denom . canonical . toLowerCase ( ) === d
)
if ( ! found ) {
throw (
` Could not find a denomination \` ${ d } \` for quantity ${ quantity } at ${ ctx } . Perhaps you meant to use on of ` +
loaded . denominations . map ( ( d ) = > d . canonical ) . join ( ", " )
)
}
return found
}
2024-06-16 16:06:26 +02:00
if ( ! Array . isArray ( toLoad . denominations ) ) {
throw (
"toLoad is not an array. Did you forget the [ and ] around the denominations at " +
ctx +
"?"
)
2024-05-27 18:31:30 +02:00
}
2023-12-12 03:46:51 +01:00
const denoms = toLoad . denominations
. map ( ( d ) = > d . toLowerCase ( ) )
. map ( ( d ) = > fetchDenom ( d ) )
2024-06-16 16:06:26 +02:00
. map ( ( d ) = > d . withValidator ( validator ) )
2023-12-12 03:46:51 +01:00
if ( toLoad . canonical ) {
2024-04-28 22:13:25 +02:00
const canonical = fetchDenom ( toLoad . canonical ) . withValidator ( validator )
2023-12-12 03:46:51 +01:00
denoms . unshift ( canonical . withBlankCanonical ( ) )
}
2024-06-16 16:06:26 +02:00
units . push (
new Unit (
loaded . quantity ,
[ key ] ,
denoms ,
loaded . eraseInvalid ,
validator ,
toLoad [ "inverted" ]
)
)
2023-12-12 03:46:51 +01:00
}
return units
2021-09-13 01:21:47 +02:00
}
2021-11-07 16:34:51 +01:00
2021-08-07 23:11:34 +02:00
isApplicableToKey ( key : string | undefined ) : boolean {
if ( key === undefined ) {
return false
}
return this . appliesToKeys . has ( key )
}
/ * *
* Finds which denomination is applicable and gives the stripped value back
* /
2022-08-18 19:17:15 +02:00
findDenomination ( valueWithDenom : string , country : ( ) = > string ) : [ string , Denomination ] {
2021-08-07 23:11:34 +02:00
if ( valueWithDenom === undefined ) {
return undefined
}
2022-08-18 19:17:15 +02:00
const defaultDenom = this . getDefaultDenomination ( country )
2021-08-07 23:11:34 +02:00
for ( const denomination of this . denominationsSorted ) {
2024-06-16 16:06:26 +02:00
const bare = denomination . StrippedValue (
valueWithDenom ,
defaultDenom === denomination ,
this . inverted
)
2021-08-07 23:11:34 +02:00
if ( bare !== null ) {
return [ bare , denomination ]
}
}
return [ undefined , undefined ]
}
2023-12-12 03:46:51 +01:00
asHumanLongValue ( value : string , country : ( ) = > string ) : BaseUIElement | string {
2021-08-07 23:11:34 +02:00
if ( value === undefined ) {
return undefined
}
2022-08-18 19:17:15 +02:00
const [ stripped , denom ] = this . findDenomination ( value , country )
2024-04-28 23:04:09 +02:00
const human = denom ? . human
2024-06-16 16:06:26 +02:00
if ( this . inverted ) {
return human . Subs ( { quantity : stripped + "/" } )
2024-04-28 23:04:09 +02:00
}
2023-12-12 03:46:51 +01:00
if ( stripped === "1" ) {
return denom ? . humanSingular ? ? stripped
}
2021-08-07 23:11:34 +02:00
if ( human === undefined ) {
2023-12-12 03:46:51 +01:00
return stripped ? ? value
2021-08-07 23:11:34 +02:00
}
2024-02-12 14:48:05 +01:00
return human . Subs ( { quantity : stripped } )
2021-08-07 23:11:34 +02:00
}
2023-12-12 03:46:51 +01:00
public toOsm ( value : string , denomination : string ) {
const denom = this . denominations . find ( ( d ) = > d . canonical === denomination )
2024-06-16 16:06:26 +02:00
if ( this . inverted ) {
return value + "/" + denom . _canonicalSingular
2024-04-28 23:04:09 +02:00
}
2023-12-12 03:46:51 +01:00
const space = denom . addSpace ? " " : ""
if ( denom . prefix ) {
return denom . canonical + space + value
2021-08-07 23:11:34 +02:00
}
2023-12-12 03:46:51 +01:00
return value + space + denom . canonical
2021-08-07 23:11:34 +02:00
}
2022-09-08 21:40:48 +02:00
2024-04-28 23:04:09 +02:00
public getDefaultDenomination ( country : ( ) = > string ) : Denomination {
2022-08-18 19:17:15 +02:00
for ( const denomination of this . denominations ) {
2023-12-12 03:46:51 +01:00
if ( denomination . useIfNoUnitGiven === true ) {
2022-08-18 19:17:15 +02:00
return denomination
}
if (
denomination . useIfNoUnitGiven === undefined ||
denomination . useIfNoUnitGiven === false
) {
continue
}
2023-06-11 01:32:30 +02:00
let countries : string | string [ ] = country ( ) ? ? [ ]
2022-08-18 19:17:15 +02:00
if ( typeof countries === "string" ) {
countries = countries . split ( "," )
}
const denominationCountries : string [ ] = denomination . useIfNoUnitGiven
if ( countries . some ( ( country ) = > denominationCountries . indexOf ( country ) >= 0 ) ) {
return denomination
}
}
2023-12-12 03:46:51 +01:00
for ( const denomination of this . denominations ) {
if ( denomination . canonical === "" ) {
return denomination
}
}
2022-08-18 19:17:15 +02:00
return this . denominations [ 0 ]
}
2021-08-07 23:11:34 +02:00
}