Rework units to allow picking different default units in different locations, fixes #1011

This commit is contained in:
Pieter Vander Vennet 2022-08-18 19:17:15 +02:00
parent e981abd2aa
commit 5da76b9418
17 changed files with 149 additions and 100 deletions

View file

@ -1,21 +1,22 @@
import {Translation} from "../UI/i18n/Translation";
import {ApplicableUnitJson} from "./ThemeConfig/Json/UnitConfigJson";
import {DenominationConfigJson} from "./ThemeConfig/Json/UnitConfigJson";
import Translations from "../UI/i18n/Translations";
import {Store, UIEventSource} from "../Logic/UIEventSource";
import {Store} from "../Logic/UIEventSource";
import BaseUIElement from "../UI/BaseUIElement";
import Toggle from "../UI/Input/Toggle";
export class Denomination {
public readonly canonical: string;
public readonly _canonicalSingular: string;
public readonly default: boolean;
public readonly useAsDefaultInput: boolean | string[]
public readonly useIfNoUnitGiven : boolean | string[]
public readonly prefix: boolean;
public readonly alternativeDenominations: string [];
private readonly _human: Translation;
private readonly _humanSingular?: Translation;
constructor(json: ApplicableUnitJson, context: string) {
constructor(json: DenominationConfigJson, context: string) {
context = `${context}.unit(${json.canonicalDenomination})`
this.canonical = json.canonicalDenomination.trim()
if (this.canonical === undefined) {
@ -32,8 +33,12 @@ export class Denomination {
this.alternativeDenominations = json.alternativeDenomination?.map(v => v.trim()) ?? []
this.default = json.default ?? false;
if(json["default"] !== undefined) {
throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead`
}
this.useIfNoUnitGiven = json.useIfNoUnitGiven
this.useAsDefaultInput = json.useAsDefaultInput ?? json.useIfNoUnitGiven
this._human = Translations.T(json.human, context + "human")
this._humanSingular = Translations.T(json.humanSingular, context + "humanSingular")
@ -68,32 +73,31 @@ export class Denomination {
* const unit = new Denomination({
* canonicalDenomination: "m",
* alternativeDenomination: ["meter"],
* 'default': true,
* human: {
* en: "meter"
* }
* }, "test")
* unit.canonicalValue("42m") // =>"42 m"
* unit.canonicalValue("42") // =>"42 m"
* unit.canonicalValue("42 m") // =>"42 m"
* unit.canonicalValue("42 meter") // =>"42 m"
*
* unit.canonicalValue("42m", true) // =>"42 m"
* unit.canonicalValue("42", true) // =>"42 m"
* unit.canonicalValue("42 m", true) // =>"42 m"
* unit.canonicalValue("42 meter", true) // =>"42 m"
* unit.canonicalValue("42m", true) // =>"42 m"
* unit.canonicalValue("42", true) // =>"42 m"
*
* // Should be trimmed if canonical is empty
* const unit = new Denomination({
* canonicalDenomination: "",
* alternativeDenomination: ["meter","m"],
* 'default': true,
* human: {
* en: "meter"
* }
* }, "test")
* unit.canonicalValue("42m") // =>"42"
* unit.canonicalValue("42") // =>"42"
* unit.canonicalValue("42 m") // =>"42"
* unit.canonicalValue("42 meter") // =>"42"
* unit.canonicalValue("42m", true) // =>"42"
* unit.canonicalValue("42", true) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42"
* unit.canonicalValue("42 meter", true) // =>"42"
*/
public canonicalValue(value: string, actAsDefault?: boolean) : string {
public canonicalValue(value: string, actAsDefault: boolean) : string {
if (value === undefined) {
return undefined;
}
@ -114,7 +118,7 @@ export class Denomination {
*
* Returns null if it doesn't match this unit
*/
public StrippedValue(value: string, actAsDefault?: boolean): string {
public StrippedValue(value: string, actAsDefault: boolean): string {
if (value === undefined) {
return undefined;
@ -153,15 +157,26 @@ export class Denomination {
}
}
if (this.default || actAsDefault) {
const parsed = Number(value.trim())
if (!isNaN(parsed)) {
return value.trim();
}
if (!actAsDefault) {
return null
}
const parsed = Number(value.trim())
if (!isNaN(parsed)) {
return value.trim();
}
return null;
}
isDefaultUnit(country: () => string) {
if(this.useIfNoUnitGiven === true){
return true
}
if(this.useIfNoUnitGiven === false){
return false
}
return this.useIfNoUnitGiven.indexOf(country()) >= 0
}
}

View file

@ -14,13 +14,32 @@ export default interface UnitConfigJson {
/**
* The possible denominations
*/
applicableUnits: ApplicableUnitJson[]
applicableUnits: DenominationConfigJson[]
}
export interface ApplicableUnitJson {
export interface DenominationConfigJson {
/**
* The canonical value which will be added to the value in OSM.
* If this evaluates to true and the value to interpret has _no_ unit given, assumes that this unit is meant.
* Alternatively, a list of country codes can be given where this acts as the default interpretation
*
* E.g., a denomination using "meter" would probably set this flag to "true";
* a denomination for "mp/h" will use the condition "_country=gb" to indicate that it is the default in the UK.
*
* If none of the units indicate that they are the default, the first denomination will be used instead
*/
useIfNoUnitGiven?: boolean | string[]
/**
* Use this value as default denomination when the user inputs a value (e.g. to force using 'centimeters' instead of 'meters' by default).
* If unset for all values, this will use 'useIfNoUnitGiven'. If at least one denomination has this set, this will default to false
*/
useAsDefaultInput?: boolean | string[]
/**
* The canonical value for this denomination which will be added to the value in OSM.
* e.g. "m" for meters
* If the user inputs '42', the canonical value will be added and it'll become '42m'.
*
@ -28,8 +47,11 @@ export interface ApplicableUnitJson {
* In this case, an empty string should be used
*/
canonicalDenomination: string,
/**
* The canonical denomination in the case that the unit is precisely '1'
* The canonical denomination in the case that the unit is precisely '1'.
* Used for display purposes
*/
canonicalDenominationSingular?: string,
@ -63,9 +85,5 @@ export interface ApplicableUnitJson {
*/
prefix?: boolean
/**
* The default interpretation - only one can be set.
* If none is set, the first unit will be considered the default interpretation of a value without a unit
*/
default?: boolean
}

View file

@ -8,14 +8,11 @@ export class Unit {
public readonly appliesToKeys: Set<string>;
public readonly denominations: Denomination[];
public readonly denominationsSorted: Denomination[];
public readonly defaultDenom: Denomination;
public readonly eraseInvalid: boolean;
private readonly possiblePostFixes: string[] = []
constructor(appliesToKeys: string[], applicableUnits: Denomination[], eraseInvalid: boolean) {
constructor(appliesToKeys: string[], applicableDenominations: Denomination[], eraseInvalid: boolean) {
this.appliesToKeys = new Set(appliesToKeys);
this.denominations = applicableUnits;
this.defaultDenom = applicableUnits.filter(denom => denom.default)[0]
this.denominations = applicableDenominations;
this.eraseInvalid = eraseInvalid
const seenUnitExtensions = new Set<string>();
@ -52,8 +49,6 @@ export class Unit {
addPostfixesOf(denomination._canonicalSingular)
denomination.alternativeDenominations.forEach(addPostfixesOf)
}
this.possiblePostFixes = Array.from(possiblePostFixes)
this.possiblePostFixes.sort((a, b) => b.length - a.length)
}
@ -71,16 +66,12 @@ export class Unit {
}
// Some keys do have unit handling
const defaultSet = json.applicableUnits.filter(u => u.default === true)
// No default is defined - we pick the first as default
if (defaultSet.length === 0) {
json.applicableUnits[0].default = true
}
// Check that there are not multiple defaults
if (defaultSet.length > 1) {
throw `Multiple units are set as default: they have canonical values of ${defaultSet.map(u => u.canonicalDenomination).join(", ")}`
if(json.applicableUnits.some(denom => denom.useAsDefaultInput !== undefined)){
json.applicableUnits.forEach(denom => {
denom.useAsDefaultInput = denom.useAsDefaultInput ?? false
})
}
const applicable = json.applicableUnits.map((u, i) => new Denomination(u, `${ctx}.units[${i}]`))
return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false)
}
@ -96,12 +87,13 @@ export class Unit {
/**
* Finds which denomination is applicable and gives the stripped value back
*/
findDenomination(valueWithDenom: string): [string, Denomination] {
findDenomination(valueWithDenom: string, country: () => string): [string, Denomination] {
if (valueWithDenom === undefined) {
return undefined;
}
const defaultDenom = this.getDefaultDenomination(country)
for (const denomination of this.denominationsSorted) {
const bare = denomination.StrippedValue(valueWithDenom)
const bare = denomination.StrippedValue(valueWithDenom, defaultDenom === denomination)
if (bare !== null) {
return [bare, denomination]
}
@ -109,11 +101,11 @@ export class Unit {
return [undefined, undefined]
}
asHumanLongValue(value: string): BaseUIElement {
asHumanLongValue(value: string, country: () => string): BaseUIElement {
if (value === undefined) {
return undefined;
}
const [stripped, denom] = this.findDenomination(value)
const [stripped, denom] = this.findDenomination(value, country)
const human = stripped === "1" ? denom?.humanSingular : denom?.human
if (human === undefined) {
return new FixedUiElement(stripped ?? value);
@ -124,24 +116,46 @@ export class Unit {
}
/**
* Returns the value without any (sub)parts of any denomination - usefull as preprocessing step for validating inputs.
* E.g.
* if 'megawatt' is a possible denomination, then '5 Meg' will be rewritten to '5' (which can then be validated as a valid pnat)
*
* Returns the original string if nothign matches
*/
stripUnitParts(str: string) {
if (str === undefined) {
return undefined;
}
for (const denominationPart of this.possiblePostFixes) {
if (str.endsWith(denominationPart)) {
return str.substring(0, str.length - denominationPart.length).trim()
public getDefaultInput(country: () => string | string[]) {
console.log("Searching the default denomination for input", country)
for (const denomination of this.denominations) {
if (denomination.useAsDefaultInput === true) {
return denomination
}
if (denomination.useAsDefaultInput === undefined || denomination.useAsDefaultInput === false) {
continue
}
let countries: string | string[] = country()
if (typeof countries === "string") {
countries = countries.split(",")
}
const denominationCountries: string[] = denomination.useAsDefaultInput
if (countries.some(country => denominationCountries.indexOf(country) >= 0)) {
return denomination
}
}
return str;
return this.denominations[0]
}
public getDefaultDenomination(country: () => string){
for (const denomination of this.denominations) {
if (denomination.useIfNoUnitGiven === true || denomination.canonical === "") {
return denomination
}
if (denomination.useIfNoUnitGiven === undefined || denomination.useIfNoUnitGiven === false) {
continue
}
let countries: string | string[] = country()
if (typeof countries === "string") {
countries = countries.split(",")
}
const denominationCountries: string[] = denomination.useIfNoUnitGiven
if (countries.some(country => denominationCountries.indexOf(country) >= 0)) {
return denomination
}
}
return this.denominations[0]
}
}