From d9cc99c44723538b29e8c96336891081861c3ba4 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 27 Jul 2021 13:44:05 +0200 Subject: [PATCH] Add comparison tagsfilter with <=, >=, < and > --- Customizations/JSON/FromJSON.ts | 37 +++++++++++++-- Docs/Tags_format.md | 10 +++++ Logic/Tags/ComparingTag.ts | 42 ++++++++++++++++++ Logic/Tags/Tag.ts | 6 --- test/Tag.spec.ts | 79 +++++++++++++++++++++------------ 5 files changed, 137 insertions(+), 37 deletions(-) create mode 100644 Logic/Tags/ComparingTag.ts diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index d51c972a4..c20d183ae 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -6,12 +6,13 @@ import {And} from "../../Logic/Tags/And"; import {Tag} from "../../Logic/Tags/Tag"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import SubstitutingTag from "../../Logic/Tags/SubstitutingTag"; +import ComparingTag from "../../Logic/Tags/ComparingTag"; export class FromJSON { public static SimpleTag(json: string, context?: string): Tag { const tag = Utils.SplitFirst(json, "="); - if(tag.length !== 2){ + if (tag.length !== 2) { throw `Invalid tag: no (or too much) '=' found (in ${context ?? "unkown context"})` } return new Tag(tag[0], tag[1]); @@ -26,6 +27,15 @@ export class FromJSON { } } + private static comparators + : [string, (a: number, b: number) => boolean][] + = [ + ["<=", (a, b) => a <= b], + [">=", (a, b) => a >= b], + ["<", (a, b) => a < b], + [">", (a, b) => a > b], + ] + private static TagUnsafe(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { if (json === undefined) { @@ -33,6 +43,27 @@ export class FromJSON { } if (typeof (json) == "string") { const tag = json as string; + + for (const [operator, comparator] of FromJSON.comparators) { + if (tag.indexOf(operator) >= 0) { + const split = Utils.SplitFirst(tag, operator); + + const val = Number(split[1].trim()) + if (isNaN(val)) { + throw `Error: not a valid value for a comparison: ${split[1]}, make sure it is a number and nothing more (at ${context})` + } + + const f = (value: string | undefined) => { + const b = Number(value?.replace(/[^\d.]/g,'')) + if (isNaN(b)) { + return false; + } + return comparator(b, val) + } + return new ComparingTag(split[0], f, operator + val) + } + } + if (tag.indexOf("!~") >= 0) { const split = Utils.SplitFirst(tag, "!~"); if (split[1] === "*") { @@ -54,11 +85,11 @@ export class FromJSON { new RegExp("^" + split[1] + "$") ); } - if(tag.indexOf(":=") >= 0){ + if (tag.indexOf(":=") >= 0) { const split = Utils.SplitFirst(tag, ":="); return new SubstitutingTag(split[0], split[1]); } - + if (tag.indexOf("!=") >= 0) { const split = Utils.SplitFirst(tag, "!="); if (split[1] === "*") { diff --git a/Docs/Tags_format.md b/Docs/Tags_format.md index f6968f615..7fd6f52d1 100644 --- a/Docs/Tags_format.md +++ b/Docs/Tags_format.md @@ -29,6 +29,16 @@ To check if a key does _not_ equal a certain value, use `key!=value`. This is co This implies that, to check if a key is present, `key!=` can be used. This will only match if the key is present and not empty. +Number comparison +----------------- + +If the value of a tag is a number (e.g. `key=42`), one can use a filter `key<=42`, `key>=35`, `key>40` or `key<50` to match this, e.g. in conditions for renderings. +These tags cannot be used to generate an answer nor can they be used to request data upstream from overpass. + +Note that the value coming from OSM will first be stripped by removing all non-numeric characters. For example, `length=42 meter` will be interpreted as `length=42` and will thus match `length<=42` and `length>=42`. +In special circumstances (e.g. `surface_area=42 m2` or `length=100 feet`), this will result in erronous values (`surface=422` or if a length in meters is compared to). +However, this can be partially alleviated by using 'Units' to rewrite to a default format. + Regex equals ------------ diff --git a/Logic/Tags/ComparingTag.ts b/Logic/Tags/ComparingTag.ts new file mode 100644 index 000000000..7e6951988 --- /dev/null +++ b/Logic/Tags/ComparingTag.ts @@ -0,0 +1,42 @@ +import {TagsFilter} from "./TagsFilter"; + +export default class ComparingTag implements TagsFilter { + private readonly _key: string; + private readonly _predicate: (value: string) => boolean; + private readonly _representation: string; + + constructor(key: string, predicate : (value:string | undefined) => boolean, representation: string = "") { + this._key = key; + this._predicate = predicate; + this._representation = representation; + } + + asChange(properties: any): { k: string; v: string }[] { + throw "A comparable tag can not be used to be uploaded to OSM" + } + + asHumanString(linkToWiki: boolean, shorten: boolean, properties: any) { + return this._key+this._representation + } + + asOverpass(): string[] { + throw "A comparable tag can not be used as overpass filter" + } + + isEquivalent(other: TagsFilter): boolean { + return other === this; + } + + isUsableAsAnswer(): boolean { + return false; + } + + matchesProperties(properties: any): boolean { + return this._predicate(properties[this._key]); + } + + usedKeys(): string[] { + return [this._key]; + } + +} \ No newline at end of file diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index c16833ae5..5dbb14d01 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -1,7 +1,6 @@ import {Utils} from "../../Utils"; import {RegexTag} from "./RegexTag"; import {TagsFilter} from "./TagsFilter"; -import {TagUtils} from "./TagUtils"; export class Tag extends TagsFilter { public key: string @@ -46,11 +45,6 @@ export class Tag extends TagsFilter { } return [`["${this.key}"="${this.value}"]`]; } - - substituteValues(tags: any) { - return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags)); - } - asHumanString(linkToWiki?: boolean, shorten?: boolean) { let v = this.value; if (shorten) { diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 4c32dc2e7..05342085b 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -4,20 +4,17 @@ import T from "./TestHelper"; import {FromJSON} from "../Customizations/JSON/FromJSON"; import Locale from "../UI/i18n/Locale"; import Translations from "../UI/i18n/Translations"; -import {UIEventSource} from "../Logic/UIEventSource"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; -import EditableTagRendering from "../UI/Popup/EditableTagRendering"; import {Translation} from "../UI/i18n/Translation"; import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours"; -import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput"; -import {SubstitutedTranslation} from "../UI/SubstitutedTranslation"; import {Tag} from "../Logic/Tags/Tag"; import {And} from "../Logic/Tags/And"; +import {centerOfMass} from "@turf/turf"; Utils.runningFromConsole = true; -export default class TagSpec extends T{ - +export default class TagSpec extends T { + constructor() { super("Tags", [ ["Tag replacement works in translation", () => { @@ -88,11 +85,37 @@ export default class TagSpec extends T{ equal(assign.matchesProperties({"survey:date": "2021-03-29"}), false); equal(assign.matchesProperties({"_date:now": "2021-03-29"}), false); equal(assign.matchesProperties({"some_key": "2021-03-29"}), false); - + const notEmptyList = FromJSON.Tag("xyz!~\\[\\]") - equal(notEmptyList.matchesProperties({"xyz":undefined}), true); - equal(notEmptyList.matchesProperties({"xyz":"[]"}), false); - equal(notEmptyList.matchesProperties({"xyz":"[\"abc\"]"}), true); + equal(notEmptyList.matchesProperties({"xyz": undefined}), true); + equal(notEmptyList.matchesProperties({"xyz": "[]"}), false); + equal(notEmptyList.matchesProperties({"xyz": "[\"abc\"]"}), true); + + let compare = FromJSON.Tag("key<=5") + equal(compare.matchesProperties({"key": undefined}), false); + equal(compare.matchesProperties({"key": "6"}), false); + equal(compare.matchesProperties({"key": "5"}), true); + equal(compare.matchesProperties({"key": "4"}), true); + + + compare = FromJSON.Tag("key<5") + equal(compare.matchesProperties({"key": undefined}), false); + equal(compare.matchesProperties({"key": "6"}), false); + equal(compare.matchesProperties({"key": "5"}), false); + equal(compare.matchesProperties({"key": "4.2"}), true); + + compare = FromJSON.Tag("key>5") + equal(compare.matchesProperties({"key": undefined}), false); + equal(compare.matchesProperties({"key": "6"}), true); + equal(compare.matchesProperties({"key": "5"}), false); + equal(compare.matchesProperties({"key": "4.2"}), false); + compare = FromJSON.Tag("key>=5") + equal(compare.matchesProperties({"key": undefined}), false); + equal(compare.matchesProperties({"key": "6"}), true); + equal(compare.matchesProperties({"key": "5"}), true); + equal(compare.matchesProperties({"key": "4.2"}), false); + + })], @@ -358,27 +381,27 @@ export default class TagSpec extends T{ ]); equal(rules, "Tu 10:00-12:00; Su 13:00-17:00"); }], - ["JOIN OH with end hours", () =>{ + ["JOIN OH with end hours", () => { const rules = OH.ToString( OH.MergeTimes([ - { - weekday: 1, - endHour: 23, - endMinutes: 30, - startHour: 23, - startMinutes: 0 - }, { - weekday: 1, - endHour: 24, - endMinutes: 0, - startHour: 23, - startMinutes: 30 - }, + { + weekday: 1, + endHour: 23, + endMinutes: 30, + startHour: 23, + startMinutes: 0 + }, { + weekday: 1, + endHour: 24, + endMinutes: 0, + startHour: 23, + startMinutes: 30 + }, - ])); + ])); equal(rules, "Tu 23:00-00:00"); - }], ["JOIN OH with overflowed hours", () =>{ + }], ["JOIN OH with overflowed hours", () => { const rules = OH.ToString( OH.MergeTimes([ @@ -464,10 +487,10 @@ export default class TagSpec extends T{ ] }; - const tagRendering = new TagRenderingConfig(config, null, "test"); + const tagRendering = new TagRenderingConfig(config, null, "test"); equal(true, tagRendering.IsKnown({bottle: "yes"})) equal(false, tagRendering.IsKnown({})) }]]); } - + }