From 514c01dc3fabd93f7adaae687e1965e8eef21d82 Mon Sep 17 00:00:00 2001 From: Stepland <16676308+Stepland@users.noreply.github.com> Date: Thu, 16 Dec 2021 19:56:37 +0100 Subject: [PATCH] Allow decimal numbers to be expressed as strings Fixes #24 --- docs/source/schema.md | 17 ++++--- docs/source/validation.md | 14 ++---- schema.json | 45 ++++++++++++++++--- .../pass/good object.json | 2 +- .../fail/negative BPM number.json | 10 +++++ .../fail/negative BPM string copy.json | 10 +++++ ...iew object with decimal zero duration.json | 14 ++++++ ...ew object with negative duration copy.json | 14 ++++++ ...review object with zero duration copy.json | 14 ++++++ .../fail/zero BPM decimal string.json | 10 +++++ .../fail/zero BPM number.json | 10 +++++ .../fail/zero BPM string.json | 10 +++++ .../pass/decimal string BPM 1.json | 10 +++++ .../pass/decimal string BPM 2.json | 10 +++++ .../pass/decimal string offset 1.json | 10 +++++ .../pass/decimal string offset 2.json | 10 +++++ .../pass/integer BPM.json | 10 +++++ .../pass/integer string BPM.json | 10 +++++ .../pass/number BPM.json | 10 +++++ .../pass/preview object.json | 14 ++++++ tests/utils.py | 2 +- 21 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 tests/data/06 - decimals as string/fail/negative BPM number.json create mode 100644 tests/data/06 - decimals as string/fail/negative BPM string copy.json create mode 100644 tests/data/06 - decimals as string/fail/preview object with decimal zero duration.json create mode 100644 tests/data/06 - decimals as string/fail/preview object with negative duration copy.json create mode 100644 tests/data/06 - decimals as string/fail/preview object with zero duration copy.json create mode 100644 tests/data/06 - decimals as string/fail/zero BPM decimal string.json create mode 100644 tests/data/06 - decimals as string/fail/zero BPM number.json create mode 100644 tests/data/06 - decimals as string/fail/zero BPM string.json create mode 100644 tests/data/06 - decimals as string/pass/decimal string BPM 1.json create mode 100644 tests/data/06 - decimals as string/pass/decimal string BPM 2.json create mode 100644 tests/data/06 - decimals as string/pass/decimal string offset 1.json create mode 100644 tests/data/06 - decimals as string/pass/decimal string offset 2.json create mode 100644 tests/data/06 - decimals as string/pass/integer BPM.json create mode 100644 tests/data/06 - decimals as string/pass/integer string BPM.json create mode 100644 tests/data/06 - decimals as string/pass/number BPM.json create mode 100644 tests/data/06 - decimals as string/pass/preview object.json diff --git a/docs/source/schema.md b/docs/source/schema.md index 245febc..a15dacc 100644 --- a/docs/source/schema.md +++ b/docs/source/schema.md @@ -54,11 +54,14 @@ Contains information that applies to the whole set of charts - string, optional - Relative path to the jacket / album cover / album art to be shown in music select for example. usually a square image. - **BPM** - - number, required + - number or string, required - Song tempo in Beats per Minute. + - Striclty positive + - Strings allowed for easier decimal representation preservation - **offset** - - number, required + - number or string, required - In seconds, opposite of the time position of the first beat in the music file. For instance, if the first beat occurs at 0.15 seconds in the audio file, `offset̀` should be -0.15 + - Strings allowed for easier decimal representation preservation - **preview** - object or string, optional - Contains either a {ref}`preview object ` or a path to a bms-style preview file @@ -76,11 +79,15 @@ Contains information that applies to the whole set of charts Describes the part of the music file that's meant to be played on loop when previewing this song at the music select screen - **start** - - number, required + - number or string, required - In seconds, start of the loop + - Positive + - Strings allowed for easier decimal representation preservation - **duration** - - number, required + - number or string, required - In seconds, duration of the loop + - Strictly positive + - Strings allowed for easier decimal representation preservation (data)= ## Data @@ -165,7 +172,7 @@ A classic note. Ticks are fractions of the beat. The resolution defines how many ticks are in a beat for a given chart. - In other words if the resolution is 420, a tick lasts for 1/420th of a beat + In other words if the resolution is 420, a tick lasts for 1/420th oLike for the BPM, sf a beat For more info about measuring time in ticks, see [bmson's docs](https://bmson-spec.readthedocs.io/en/master/doc/index.html#terminologies) (their docs refers to ticks as *pulses*). - as an array : diff --git a/docs/source/validation.md b/docs/source/validation.md index b59a919..45ad2fe 100644 --- a/docs/source/validation.md +++ b/docs/source/validation.md @@ -103,19 +103,13 @@ Since this is mostly an implicit rule that's not strictly followed by official c ## Decimal values -If possible, non-integer values like `BPM` or `offset` should be manipulated using a [Decimal Data Type](https://en.wikipedia.org/wiki/Decimal_data_type) to preserve their original decimal representation. I think no one likes to see the BPM they defined as a clean `195.3` in the editor be stored as a messy `195.3000030517578125` in the file. +If possible, non-integer values like `BPM` or `offset` should be manipulated using a [Decimal Data Type](https://en.wikipedia.org/wiki/Decimal_data_type) to preserve their original decimal representation. I think no one likes to see the BPM they defined as a clean `195.3` in the editor be stored as a messy `195.3000030517578125` in the file. If doing this is too hard with your language / library of choice, keep in mind that `1.0.0` allows strings instead : -Be careful that the serialized values in the resulting file must still be *number litterals*, not strings. - -- **Good** +- **Valid** ```json { "BPM" : 195.3 } ``` -- **Bad** +- **Valid since 1.0.0** ```json { "BPM": "195.3" } - ``` - -This is not that easy. Python's standard module `json`, for instance, allows *deserializing* (reading) numbers in a json file as `decimal.Decimal` instances, but has no easy way of *serializing* (writing) `decimal.Decimal` instances as a json number litteral that respects the original representation. - -A possible solution is to use the [simplejson](https://pypi.org/project/simplejson/) module as a near drop-in replacement for `json` and use the `use_decimal` options \ No newline at end of file + ``` \ No newline at end of file diff --git a/schema.json b/schema.json index e2285d6..95247a8 100644 --- a/schema.json +++ b/schema.json @@ -30,12 +30,11 @@ }, "BPM": { "description": "Song tempo in Beats per Minute", - "type": "number", - "exclusiveMinimum": 0 + "$ref": "#/$defs/strictlyPositiveDecimal" }, "offset": { "description": "In seconds, opposite of the time position of the first beat in the music file", - "type": "number" + "$ref": "#/$defs/decimal" }, "preview": { "description": "Describes the part of the music file that's to be played on loop when previewing this song", @@ -45,13 +44,11 @@ "properties": { "start": { "description": "In seconds, Time at which preview should start", - "type": "number", - "minimum": 0 + "$ref": "#/$defs/positiveDecimal" }, "duration": { "description": "In seconds, for how long should the preview be played past the starting point", - "type": "number", - "minimum": 0 + "$ref": "#/$defs/strictlyPositiveDecimal" } }, "required": ["start", "duration"] @@ -180,6 +177,40 @@ } } ] + }, + "decimal": { + "description": "a decimal number as either a number literal or a string", + "oneOf": [{ + "type": "number" + }, + { + "type": "string", + "pattern": "^-?\\d+(\\.\\d+)?$" + } + ] + }, + "positiveDecimal": { + "description": "a decimal number as either a number literal or a string", + "oneOf": [{ + "type": "number" + }, + { + "type": "string", + "pattern": "^\\d+(\\.\\d+)?$" + } + ] + }, + "strictlyPositiveDecimal": { + "description": "a strictly positive decimal number as either a number literal or a string", + "oneOf": [{ + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "string", + "pattern": "^(0\\.\\d*[1-9]\\d*|\\d*[1-9]\\d*(\\.\\d+)?)$" + } + ] } } } \ No newline at end of file diff --git a/tests/data/02 - polymorphic preview/pass/good object.json b/tests/data/02 - polymorphic preview/pass/good object.json index 8e9ad3e..5a4d123 100644 --- a/tests/data/02 - polymorphic preview/pass/good object.json +++ b/tests/data/02 - polymorphic preview/pass/good object.json @@ -7,7 +7,7 @@ "offset": 0, "preview": { "start": 0, - "duration": 0 + "duration": 1 } }, "data": {} diff --git a/tests/data/06 - decimals as string/fail/negative BPM number.json b/tests/data/06 - decimals as string/fail/negative BPM number.json new file mode 100644 index 0000000..d144056 --- /dev/null +++ b/tests/data/06 - decimals as string/fail/negative BPM number.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": -1, + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/fail/negative BPM string copy.json b/tests/data/06 - decimals as string/fail/negative BPM string copy.json new file mode 100644 index 0000000..ca92578 --- /dev/null +++ b/tests/data/06 - decimals as string/fail/negative BPM string copy.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": "-1", + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/fail/preview object with decimal zero duration.json b/tests/data/06 - decimals as string/fail/preview object with decimal zero duration.json new file mode 100644 index 0000000..ea89e21 --- /dev/null +++ b/tests/data/06 - decimals as string/fail/preview object with decimal zero duration.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 1, + "offset": 0, + "preview": { + "start": "0", + "duration": "0.000" + } + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/fail/preview object with negative duration copy.json b/tests/data/06 - decimals as string/fail/preview object with negative duration copy.json new file mode 100644 index 0000000..c614ddd --- /dev/null +++ b/tests/data/06 - decimals as string/fail/preview object with negative duration copy.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 1, + "offset": 0, + "preview": { + "start": "0", + "duration": "-10.000" + } + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/fail/preview object with zero duration copy.json b/tests/data/06 - decimals as string/fail/preview object with zero duration copy.json new file mode 100644 index 0000000..a602579 --- /dev/null +++ b/tests/data/06 - decimals as string/fail/preview object with zero duration copy.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 1, + "offset": 0, + "preview": { + "start": "0", + "duration": "0" + } + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/fail/zero BPM decimal string.json b/tests/data/06 - decimals as string/fail/zero BPM decimal string.json new file mode 100644 index 0000000..e5f351b --- /dev/null +++ b/tests/data/06 - decimals as string/fail/zero BPM decimal string.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": "0.000", + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/fail/zero BPM number.json b/tests/data/06 - decimals as string/fail/zero BPM number.json new file mode 100644 index 0000000..72a8914 --- /dev/null +++ b/tests/data/06 - decimals as string/fail/zero BPM number.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 0, + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/fail/zero BPM string.json b/tests/data/06 - decimals as string/fail/zero BPM string.json new file mode 100644 index 0000000..ae9f892 --- /dev/null +++ b/tests/data/06 - decimals as string/fail/zero BPM string.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": "0", + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/decimal string BPM 1.json b/tests/data/06 - decimals as string/pass/decimal string BPM 1.json new file mode 100644 index 0000000..aa7ca43 --- /dev/null +++ b/tests/data/06 - decimals as string/pass/decimal string BPM 1.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": "0.1", + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/decimal string BPM 2.json b/tests/data/06 - decimals as string/pass/decimal string BPM 2.json new file mode 100644 index 0000000..4e3ae2d --- /dev/null +++ b/tests/data/06 - decimals as string/pass/decimal string BPM 2.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": "123.456", + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/decimal string offset 1.json b/tests/data/06 - decimals as string/pass/decimal string offset 1.json new file mode 100644 index 0000000..d95f800 --- /dev/null +++ b/tests/data/06 - decimals as string/pass/decimal string offset 1.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 123.456, + "offset": "-0.24" + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/decimal string offset 2.json b/tests/data/06 - decimals as string/pass/decimal string offset 2.json new file mode 100644 index 0000000..e7c0798 --- /dev/null +++ b/tests/data/06 - decimals as string/pass/decimal string offset 2.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 123.456, + "offset": "12.34" + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/integer BPM.json b/tests/data/06 - decimals as string/pass/integer BPM.json new file mode 100644 index 0000000..f31d324 --- /dev/null +++ b/tests/data/06 - decimals as string/pass/integer BPM.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 125, + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/integer string BPM.json b/tests/data/06 - decimals as string/pass/integer string BPM.json new file mode 100644 index 0000000..e0f8fb6 --- /dev/null +++ b/tests/data/06 - decimals as string/pass/integer string BPM.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": "123", + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/number BPM.json b/tests/data/06 - decimals as string/pass/number BPM.json new file mode 100644 index 0000000..2293199 --- /dev/null +++ b/tests/data/06 - decimals as string/pass/number BPM.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 123.456, + "offset": 0 + }, + "data": {} +} \ No newline at end of file diff --git a/tests/data/06 - decimals as string/pass/preview object.json b/tests/data/06 - decimals as string/pass/preview object.json new file mode 100644 index 0000000..560b1f2 --- /dev/null +++ b/tests/data/06 - decimals as string/pass/preview object.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0", + "metadata": { + "title": "", + "artist": "", + "BPM": 1, + "offset": 0, + "preview": { + "start": "8346.9346", + "duration": "35.936" + } + }, + "data": {} +} \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py index 1bba4bf..338fb02 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,7 +7,7 @@ from jschon.jsonschema import Scope from jschon import create_catalog, JSONSchema, JSON import pytest -_catalog = create_catalog('2020-12') +_catalog = create_catalog("2020-12") SCHEMA = JSONSchema.loadf("schema.json", catalog=_catalog) def prettify_path(json_path: str) -> str: