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",
"months"
]
},
"charge": {
"quantity": "duration",
"inverted": true,
"denominations": [
"days",
"weeks",
"months",
"years"
]
}
}
]

View file

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

View file

@ -110,19 +110,21 @@ export class Denomination {
* @param value the value from OSM
* @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({
* canonicalDenomination: "m",
* alternativeDenomination: ["meter"],
* human: {
* en: "{quantity} meter"
* }
* }, "test")
* 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"
* }, Validators.get("float"), "test")
* unit.canonicalValue("42m", true, false) // =>"42 m"
* unit.canonicalValue("42", true, false) // =>"42 m"
* unit.canonicalValue("42 m", true, false) // =>"42 m"
* unit.canonicalValue("42 meter", true, false) // =>"42 m"
* unit.canonicalValue("42m", true, false) // =>"42 m"
* unit.canonicalValue("42", true, false) // =>"42 m"
*
* // Should be trimmed if canonical is empty
* const unit = Denomination.fromJson({
@ -131,22 +133,26 @@ export class Denomination {
* human: {
* en: "{quantity} meter"
* }
* }, "test")
* unit.canonicalValue("42m", true) // =>"42"
* unit.canonicalValue("42", true) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42"
* unit.canonicalValue("42 meter", true) // =>"42"
* }, Validators.get("float"), "test")
* unit.canonicalValue("42m", true, false) // =>"42"
* unit.canonicalValue("42", true, false) // =>"42"
* unit.canonicalValue("42 m", true, false) // =>"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) {
return undefined
}
const stripped = this.StrippedValue(value, actAsDefault)
const stripped = this.StrippedValue(value, actAsDefault, inverted)
if (stripped === null) {
return null
}
if(inverted){
return (stripped + "/" + this.canonical).trim()
}
if (stripped === "1" && this._canonicalSingular !== undefined) {
return ("1 " + this._canonicalSingular).trim()
}
@ -160,7 +166,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, inverted: boolean): string {
if (value === undefined) {
return undefined
}
@ -178,10 +184,16 @@ export class Denomination {
function substr(key) {
if (self.prefix) {
return value.substr(key.length).trim()
} else {
return value.substring(0, value.length - key.length).trim()
return value.substring(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())) {

View file

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

View file

@ -178,7 +178,9 @@
checkedMappings,
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) {
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined

View file

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

View file

@ -1,6 +1,7 @@
import { Unit } from "../../src/Models/Unit"
import { Denomination } from "../../src/Models/Denomination"
import { describe, expect, it } from "vitest"
import Validators from "../../src/UI/InputElement/Validators"
describe("Unit", () => {
it("should convert a value back and forth", () => {
@ -13,14 +14,36 @@ describe("Unit", () => {
nl: "{quantity} megawatt",
},
},
Validators.get("float"),
"test"
)
const canonical = denomintion.canonicalValue("5", true)
const canonical = denomintion.canonicalValue("5", true, false)
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")
expect(detected).toBe("5")
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)
})
})