Units: add possibility to have an "inverted" time unit, e.g. for charge; add charge units to bike_parking
This commit is contained in:
		
							parent
							
								
									4ccfe3efe4
								
							
						
					
					
						commit
						bf523848fb
					
				
					 8 changed files with 94 additions and 32 deletions
				
			
		| 
						 | 
				
			
			@ -998,6 +998,16 @@
 | 
			
		|||
          "weeks",
 | 
			
		||||
          "months"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "charge": {
 | 
			
		||||
        "quantity": "duration",
 | 
			
		||||
        "inverted": true,
 | 
			
		||||
        "denominations": [
 | 
			
		||||
          "days",
 | 
			
		||||
          "weeks",
 | 
			
		||||
          "months",
 | 
			
		||||
          "years"
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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())) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 }>
 | 
			
		||||
    )[]
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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: "" })} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
    })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue