Units: add possibility to have an "inverted" time unit, e.g. for charge; add charge units to bike_parking

This commit is contained in:
Pieter Vander Vennet 2024-04-28 23:04:09 +02:00
parent 4ccfe3efe4
commit bf523848fb
8 changed files with 94 additions and 32 deletions

View file

@ -998,6 +998,16 @@
"weeks", "weeks",
"months" "months"
] ]
},
"charge": {
"quantity": "duration",
"inverted": true,
"denominations": [
"days",
"weeks",
"months",
"years"
]
} }
} }
] ]

View file

@ -435,7 +435,7 @@ export default class SimpleMetaTaggers {
() => feature.properties["_country"] () => feature.properties["_country"]
) )
let canonical = let canonical =
denomination?.canonicalValue(value, defaultDenom == denomination) ?? denomination?.canonicalValue(value, defaultDenom == denomination, unit.inverted) ??
undefined undefined
if (canonical === value) { if (canonical === value) {
break break

View file

@ -110,19 +110,21 @@ export class Denomination {
* @param value the value from OSM * @param value the value from OSM
* @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed * @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed
* *
* import Validators from "../UI/InputElement/Validators"
*
* const unit = Denomination.fromJson({ * const unit = Denomination.fromJson({
* canonicalDenomination: "m", * canonicalDenomination: "m",
* alternativeDenomination: ["meter"], * alternativeDenomination: ["meter"],
* human: { * human: {
* en: "{quantity} meter" * en: "{quantity} meter"
* } * }
* }, "test") * }, Validators.get("float"), "test")
* unit.canonicalValue("42m", true) // =>"42 m" * unit.canonicalValue("42m", true, false) // =>"42 m"
* unit.canonicalValue("42", true) // =>"42 m" * unit.canonicalValue("42", true, false) // =>"42 m"
* unit.canonicalValue("42 m", true) // =>"42 m" * unit.canonicalValue("42 m", true, false) // =>"42 m"
* unit.canonicalValue("42 meter", true) // =>"42 m" * unit.canonicalValue("42 meter", true, false) // =>"42 m"
* unit.canonicalValue("42m", true) // =>"42 m" * unit.canonicalValue("42m", true, false) // =>"42 m"
* unit.canonicalValue("42", true) // =>"42 m" * unit.canonicalValue("42", true, false) // =>"42 m"
* *
* // Should be trimmed if canonical is empty * // Should be trimmed if canonical is empty
* const unit = Denomination.fromJson({ * const unit = Denomination.fromJson({
@ -131,22 +133,26 @@ export class Denomination {
* human: { * human: {
* en: "{quantity} meter" * en: "{quantity} meter"
* } * }
* }, "test") * }, Validators.get("float"), "test")
* unit.canonicalValue("42m", true) // =>"42" * unit.canonicalValue("42m", true, false) // =>"42"
* unit.canonicalValue("42", true) // =>"42" * unit.canonicalValue("42", true, false) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42" * unit.canonicalValue("42 m", true, false) // =>"42"
* unit.canonicalValue("42 meter", true) // =>"42" * unit.canonicalValue("42 meter", true, false) // =>"42"
* *
* *
*/ */
public canonicalValue(value: string, actAsDefault: boolean): string { public canonicalValue(value: string, actAsDefault: boolean, inverted: boolean): string {
if (value === undefined) { if (value === undefined) {
return undefined return undefined
} }
const stripped = this.StrippedValue(value, actAsDefault) const stripped = this.StrippedValue(value, actAsDefault, inverted)
if (stripped === null) { if (stripped === null) {
return null return null
} }
if(inverted){
return (stripped + "/" + this.canonical).trim()
}
if (stripped === "1" && this._canonicalSingular !== undefined) { if (stripped === "1" && this._canonicalSingular !== undefined) {
return ("1 " + this._canonicalSingular).trim() return ("1 " + this._canonicalSingular).trim()
} }
@ -160,7 +166,7 @@ export class Denomination {
* *
* Returns null if it doesn't match this unit * Returns null if it doesn't match this unit
*/ */
public StrippedValue(value: string, actAsDefault: boolean): string { public StrippedValue(value: string, actAsDefault: boolean, inverted: boolean): string {
if (value === undefined) { if (value === undefined) {
return undefined return undefined
} }
@ -178,10 +184,16 @@ export class Denomination {
function substr(key) { function substr(key) {
if (self.prefix) { if (self.prefix) {
return value.substr(key.length).trim() return value.substring(key.length).trim()
} else {
return value.substring(0, value.length - key.length).trim()
} }
let trimmed = value.substring(0, value.length - key.length).trim()
if(!inverted){
return trimmed
}
if(trimmed.endsWith("/")){
trimmed = trimmed.substring(0, trimmed.length - 1).trim()
}
return trimmed
} }
if (this.canonical !== "" && startsWith(this.canonical.toLowerCase())) { if (this.canonical !== "" && startsWith(this.canonical.toLowerCase())) {

View file

@ -519,6 +519,7 @@ export interface LayerConfigJson {
/** /**
* Either a list with [{"key": "unitname", "key2": {"quantity": "unitname", "denominations": ["denom", "denom"]}}] * Either a list with [{"key": "unitname", "key2": {"quantity": "unitname", "denominations": ["denom", "denom"]}}]
* *
* Use `"inverted": true` if the amount should be _divided_ by the denomination, e.g. for charge over time (`€5/day`)
* *
* @see UnitConfigJson * @see UnitConfigJson
* *
@ -526,7 +527,7 @@ export interface LayerConfigJson {
*/ */
units?: ( units?: (
| UnitConfigJson | UnitConfigJson
| Record<string, string | { quantity: string; denominations: string[]; canonical?: string }> | Record<string, string | { quantity: string; denominations: string[]; canonical?: string, inverted?: boolean }>
)[] )[]
/** /**

View file

@ -15,16 +15,19 @@ export class Unit {
public readonly eraseInvalid: boolean public readonly eraseInvalid: boolean
public readonly quantity: string public readonly quantity: string
private readonly _validator: Validator private readonly _validator: Validator
public readonly inverted: boolean
constructor( constructor(
quantity: string, quantity: string,
appliesToKeys: string[], appliesToKeys: string[],
applicableDenominations: Denomination[], applicableDenominations: Denomination[],
eraseInvalid: boolean, eraseInvalid: boolean,
validator: Validator validator: Validator,
inverted: boolean = false
) { ) {
this.quantity = quantity this.quantity = quantity
this._validator = validator this._validator = validator
this.inverted = inverted
this.appliesToKeys = new Set(appliesToKeys) this.appliesToKeys = new Set(appliesToKeys)
this.denominations = applicableDenominations this.denominations = applicableDenominations
this.eraseInvalid = eraseInvalid this.eraseInvalid = eraseInvalid
@ -73,7 +76,7 @@ export class Unit {
static fromJson( static fromJson(
json: json:
| UnitConfigJson | UnitConfigJson
| Record<string, string | { quantity: string; denominations: string[] }>, | Record<string, string | { quantity: string; denominations: string[], inverted?: boolean }>,
tagRenderings: TagRenderingConfig[], tagRenderings: TagRenderingConfig[],
ctx: string ctx: string
): Unit[] { ): Unit[] {
@ -210,7 +213,7 @@ export class Unit {
private static loadFromLibrary( private static loadFromLibrary(
spec: Record< spec: Record<
string, string,
string | { quantity: string; denominations: string[]; canonical?: string } string | { quantity: string; denominations: string[]; canonical?: string, inverted?: boolean }
>, >,
types: Record<string, ValidatorType>, types: Record<string, ValidatorType>,
ctx: string ctx: string
@ -222,7 +225,7 @@ export class Unit {
if (typeof toLoad === "string") { if (typeof toLoad === "string") {
const loaded = this.getFromLibrary(toLoad, ctx) const loaded = this.getFromLibrary(toLoad, ctx)
units.push( units.push(
new Unit(loaded.quantity, [key], loaded.denominations, loaded.eraseInvalid, validator) new Unit(loaded.quantity, [key], loaded.denominations, loaded.eraseInvalid, validator, toLoad["inverted"])
) )
continue continue
} }
@ -252,7 +255,7 @@ export class Unit {
const canonical = fetchDenom(toLoad.canonical).withValidator(validator) const canonical = fetchDenom(toLoad.canonical).withValidator(validator)
denoms.unshift(canonical.withBlankCanonical()) denoms.unshift(canonical.withBlankCanonical())
} }
units.push(new Unit(loaded.quantity, [key], denoms, loaded.eraseInvalid, validator)) units.push(new Unit(loaded.quantity, [key], denoms, loaded.eraseInvalid, validator, toLoad["inverted"]))
} }
return units return units
} }
@ -274,7 +277,7 @@ export class Unit {
} }
const defaultDenom = this.getDefaultDenomination(country) const defaultDenom = this.getDefaultDenomination(country)
for (const denomination of this.denominationsSorted) { for (const denomination of this.denominationsSorted) {
const bare = denomination.StrippedValue(valueWithDenom, defaultDenom === denomination) const bare = denomination.StrippedValue(valueWithDenom, defaultDenom === denomination, this.inverted)
if (bare !== null) { if (bare !== null) {
return [bare, denomination] return [bare, denomination]
} }
@ -287,10 +290,13 @@ export class Unit {
return undefined return undefined
} }
const [stripped, denom] = this.findDenomination(value, country) const [stripped, denom] = this.findDenomination(value, country)
const human = denom?.human
if(this.inverted ){
return human.Subs({quantity: stripped+"/"})
}
if (stripped === "1") { if (stripped === "1") {
return denom?.humanSingular ?? stripped return denom?.humanSingular ?? stripped
} }
const human = denom?.human
if (human === undefined) { if (human === undefined) {
return stripped ?? value return stripped ?? value
} }
@ -300,6 +306,10 @@ export class Unit {
public toOsm(value: string, denomination: string) { public toOsm(value: string, denomination: string) {
const denom = this.denominations.find((d) => d.canonical === denomination) const denom = this.denominations.find((d) => d.canonical === denomination)
if(this.inverted){
return value+"/"+denom._canonicalSingular
}
const space = denom.addSpace ? " " : "" const space = denom.addSpace ? " " : ""
if (denom.prefix) { if (denom.prefix) {
return denom.canonical + space + value return denom.canonical + space + value
@ -307,7 +317,7 @@ export class Unit {
return value + space + denom.canonical return value + space + denom.canonical
} }
public getDefaultDenomination(country: () => string) { public getDefaultDenomination(country: () => string): Denomination {
for (const denomination of this.denominations) { for (const denomination of this.denominations) {
if (denomination.useIfNoUnitGiven === true) { if (denomination.useIfNoUnitGiven === true) {
return denomination return denomination

View file

@ -178,7 +178,9 @@
checkedMappings, checkedMappings,
tags.data tags.data
) )
console.log("Constructing change spec from", {freeform: $freeformInput, selectedMapping, checkedMappings, currentTags: tags.data}, " --> ", selectedTags) if(state.featureSwitches.featureSwitchIsDebugging.data){
console.log("Constructing change spec from", {freeform: $freeformInput, selectedMapping, checkedMappings, currentTags: tags.data}, " --> ", selectedTags)
}
} catch (e) { } catch (e) {
console.error("Could not calculate changeSpecification:", e) console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined selectedTags = undefined

View file

@ -64,10 +64,14 @@
) )
</script> </script>
{#if unit.inverted}
<div class="bold px-2">/</div>
{/if}
<select bind:value={$selectedUnit}> <select bind:value={$selectedUnit}>
{#each unit.denominations as denom (denom.canonical)} {#each unit.denominations as denom (denom.canonical)}
<option value={denom.canonical}> <option value={denom.canonical}>
{#if $isSingle} {#if $isSingle || unit.inverted}
<Tr t={denom.humanSingular} /> <Tr t={denom.humanSingular} />
{:else} {:else}
<Tr t={denom.human.Subs({ quantity: "" })} /> <Tr t={denom.human.Subs({ quantity: "" })} />

View file

@ -1,6 +1,7 @@
import { Unit } from "../../src/Models/Unit" import { Unit } from "../../src/Models/Unit"
import { Denomination } from "../../src/Models/Denomination" import { Denomination } from "../../src/Models/Denomination"
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import Validators from "../../src/UI/InputElement/Validators"
describe("Unit", () => { describe("Unit", () => {
it("should convert a value back and forth", () => { it("should convert a value back and forth", () => {
@ -13,14 +14,36 @@ describe("Unit", () => {
nl: "{quantity} megawatt", nl: "{quantity} megawatt",
}, },
}, },
Validators.get("float"),
"test" "test"
) )
const canonical = denomintion.canonicalValue("5", true) const canonical = denomintion.canonicalValue("5", true, false)
expect(canonical).toBe("5 MW") expect(canonical).toBe("5 MW")
const units = new Unit("quantity", ["key"], [denomintion], false) const units = new Unit("quantity", ["key"], [denomintion], false, Validators.get("float"))
const [detected, detectedDenom] = units.findDenomination("5 MW", () => "be") const [detected, detectedDenom] = units.findDenomination("5 MW", () => "be")
expect(detected).toBe("5") expect(detected).toBe("5")
expect(detectedDenom).toBe(denomintion) expect(detectedDenom).toBe(denomintion)
}) })
it("should convert an inverted value back and forth", () => {
const denomintion = Denomination.fromJson(
{
canonicalDenomination: "year",
human: {
en: "{quantity} year",
nl: "{quantity} year",
},
},
Validators.get("float"),
"test"
)
const canonical = denomintion.canonicalValue("5", true, true)
expect(canonical).toBe("5/year")
const unit = new Unit("quantity", ["key"], [denomintion], false, Validators.get("float"), true)
const [detected, detectedDenom] = unit.findDenomination("5/year", () => "be")
expect(detected).toBe("5")
expect(detectedDenom).toBe(denomintion)
})
}) })