From d6f6cc764780bd22142b3172132a2d1aaf622dc8 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:07:42 -0300 Subject: [PATCH 01/18] experiment with thousand sep --- oxifmt.typ | 88 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index 1283f24..b1b9249 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -244,16 +244,42 @@ type := '' | '?' | 'x?' | 'X?' | identifier count := parameter | integer parameter := argument '$' */ -#let _generate-replacement(fullname, extras, replacement, pos-replacements: (), named-replacements: (:), fmt-decimal-separator: auto) = { +#let _generate-replacement( + fullname, extras, replacement, + pos-replacements: (), named-replacements: (:), + fmt-decimal-separator: auto, + fmt-thousands-separator: "" +) = { if extras == none { - if not _strfmt_is-numeric-type(replacement) { - fmt-decimal-separator = auto - } - replacement = _strfmt_stringify(replacement) - if fmt-decimal-separator not in (auto, none) { - replacement = replacement.replace(".", _strfmt_stringify(fmt-decimal-separator)) + let is-numeric = _strfmt_is-numeric-type(replacement) + let string-replacement = _strfmt_stringify(replacement) + + if is-numeric { + let (integral, ..fractional) = string-replacement.split(".") + if fmt-thousands-separator != "" { + assert( + type(fmt-thousands-separator) == _str-type, + message: "String formatter error: 'fmt-thousands-separator' must be a string (or empty string, \"\", to disable)." + ) + integral = str( + bytes( + array(bytes(integral.rev())) + .chunks(3) + .intersperse(array(bytes(fmt-thousands-separator.rev()))) + .join() + ) + ).rev() + } + + if fractional.len() > 0 { + let decimal-separator = if fmt-decimal-separator not in (auto, none) { _strfmt_stringify(fmt-decimal-separator) } else { "." } + return integral + decimal-separator + fractional.first() + } else { + return integral + } + } else { + return string-replacement } - return replacement } let extras = _strfmt_stringify(extras) // note: usage of [\s\S] in regex to include all characters, incl. newline @@ -369,37 +395,64 @@ parameter := argument '$' // we'll add the sign back later! replacement = calc.abs(replacement) + // Separate integral from fractional parts + // We'll recompose them later + let integral = "" + let fractional = () + if spectype in ("e", "E") { let exponent-sign = if spectype == "E" { "E" } else { "e" } - replacement = _strfmt_exp-format(calc.abs(replacement), exponent-sign: exponent-sign, precision: precision) + let new-replacement = _strfmt_exp-format(calc.abs(replacement), exponent-sign: exponent-sign, precision: precision) + (integral, ..fractional) = new-replacement.split(".") } else if type(replacement) != _int-type and precision != none { - replacement = _strfmt_with-precision(replacement, precision) + let new-replacement = _strfmt_with-precision(replacement, precision) + (integral, ..fractional) = new-replacement.split(".") } else if type(replacement) == _int-type and spectype in ("x", "X", "b", "o", "x?", "X?") { let radix-map = (x: 16, X: 16, "x?": 16, "X?": 16, b: 2, o: 8) let radix = radix-map.at(spectype) let lowercase = spectype.starts-with("x") - replacement = _strfmt_stringify(_strfmt_display-radix(replacement, radix, lowercase: lowercase, signed: false)) + integral = _strfmt_stringify(_strfmt_display-radix(replacement, radix, lowercase: lowercase, signed: false)) if hashtag { let hashtag-prefix-map = ("16": "0x", "2": "0b", "8": "0o") hashtag-prefix = hashtag-prefix-map.at(str(radix)) } } else { precision = none - replacement = if spectype.ends-with("?") { + let new-replacement = if spectype.ends-with("?") { repr(replacement) } else { _strfmt_stringify(replacement) } + (integral, ..fractional) = new-replacement.split(".") } - if fmt-decimal-separator not in (auto, none) { - replacement = replacement.replace(".", _strfmt_stringify(fmt-decimal-separator)) + + if fmt-thousands-separator != "" { + assert( + type(fmt-thousands-separator) == _str-type, + message: "String formatter error: 'fmt-thousands-separator' must be a string (or empty string, \"\", to disable)." + ) + integral = str( + bytes( + array(bytes(integral.rev())) + .chunks(3) + .intersperse(array(bytes(fmt-thousands-separator.rev()))) + .join() + ) + ).rev() } + let expected-thousand-len = fmt-thousands-separator.len() * int(calc.max(0, (integral.len() - 1)) / 3) + let decimal-separator = if fmt-decimal-separator not in (auto, none) { _strfmt_stringify(fmt-decimal-separator) } else { "." } + let replaced-fractional = if fractional.len() > 0 { decimal-separator + fractional.join(decimal-separator) } else { "" } + if zero { - let width-diff = width - (replacement.len() + sign.len() + hashtag-prefix.len()) + let width-diff = width - (integral.len() + expected-thousand-len + replaced-fractional.len() + sign.len() + hashtag-prefix.len()) if width-diff > 0 { // prefix with the appropriate amount of zeroes - replacement = ("0" * width-diff) + replacement + integral = ("0" * width-diff) + integral } } + + + replacement = integral + replaced-fractional } else { sign = "" hashtag-prefix = "" @@ -449,6 +502,7 @@ parameter := argument '$' let named-replacements = replacements.named() let unnamed-format-index = 0 let fmt-decimal-separator = if "fmt-decimal-separator" in named-replacements { named-replacements.at("fmt-decimal-separator") } else { auto } + let fmt-thousands-separator = if "fmt-thousands-separator" in named-replacements { named-replacements.at("fmt-thousands-separator") } else { "" } let parts = () let last-span-end = 0 @@ -490,7 +544,7 @@ parameter := argument '$' } replace-by = named-replacements.at(name) } - replace-by = _generate-replacement(f.name, extras, replace-by, pos-replacements: num-replacements, named-replacements: named-replacements, fmt-decimal-separator: fmt-decimal-separator) + replace-by = _generate-replacement(f.name, extras, replace-by, pos-replacements: num-replacements, named-replacements: named-replacements, fmt-decimal-separator: fmt-decimal-separator, fmt-thousands-separator: fmt-thousands-separator) replace-span = f.span } else { panic("String formatter error: Internal error (unexpected format received).") From b4b393daea10dc7be360ce781da0b9ee65d99bde Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 30 Sep 2024 01:48:30 -0300 Subject: [PATCH 02/18] add fmt-thousands-count and tests, fix bugs --- oxifmt.typ | 62 ++++++++++++++++++++++++------------------ tests/strfmt-tests.typ | 26 ++++++++++++++++++ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index b1b9249..49e4dc9 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -228,7 +228,7 @@ let mantissa = f / calc.pow(10, exponent) let mantissa = _strfmt_with-precision(mantissa, precision) - mantissa + exponent-sign + _strfmt_stringify(exponent) + (mantissa, exponent-sign + _strfmt_stringify(exponent)) } // Parses {format:specslikethis}. @@ -248,6 +248,7 @@ parameter := argument '$' fullname, extras, replacement, pos-replacements: (), named-replacements: (:), fmt-decimal-separator: auto, + fmt-thousands-count: 3, fmt-thousands-separator: "" ) = { if extras == none { @@ -257,14 +258,10 @@ parameter := argument '$' if is-numeric { let (integral, ..fractional) = string-replacement.split(".") if fmt-thousands-separator != "" { - assert( - type(fmt-thousands-separator) == _str-type, - message: "String formatter error: 'fmt-thousands-separator' must be a string (or empty string, \"\", to disable)." - ) integral = str( bytes( array(bytes(integral.rev())) - .chunks(3) + .chunks(fmt-thousands-count) .intersperse(array(bytes(fmt-thousands-separator.rev()))) .join() ) @@ -399,11 +396,13 @@ parameter := argument '$' // We'll recompose them later let integral = "" let fractional = () + let exponent-suffix = "" if spectype in ("e", "E") { let exponent-sign = if spectype == "E" { "E" } else { "e" } - let new-replacement = _strfmt_exp-format(calc.abs(replacement), exponent-sign: exponent-sign, precision: precision) - (integral, ..fractional) = new-replacement.split(".") + let (mantissa, exponent) = _strfmt_exp-format(calc.abs(replacement), exponent-sign: exponent-sign, precision: precision) + (integral, ..fractional) = mantissa.split(".") + exponent-suffix = exponent } else if type(replacement) != _int-type and precision != none { let new-replacement = _strfmt_with-precision(replacement, precision) (integral, ..fractional) = new-replacement.split(".") @@ -426,33 +425,30 @@ parameter := argument '$' (integral, ..fractional) = new-replacement.split(".") } - if fmt-thousands-separator != "" { - assert( - type(fmt-thousands-separator) == _str-type, - message: "String formatter error: 'fmt-thousands-separator' must be a string (or empty string, \"\", to disable)." - ) - integral = str( - bytes( - array(bytes(integral.rev())) - .chunks(3) - .intersperse(array(bytes(fmt-thousands-separator.rev()))) - .join() - ) - ).rev() - } - let expected-thousand-len = fmt-thousands-separator.len() * int(calc.max(0, (integral.len() - 1)) / 3) let decimal-separator = if fmt-decimal-separator not in (auto, none) { _strfmt_stringify(fmt-decimal-separator) } else { "." } let replaced-fractional = if fractional.len() > 0 { decimal-separator + fractional.join(decimal-separator) } else { "" } + let exponent-suffix = exponent-suffix.replace(".", decimal-separator) if zero { - let width-diff = width - (integral.len() + expected-thousand-len + replaced-fractional.len() + sign.len() + hashtag-prefix.len()) + let width-diff = width - (integral.len() + replaced-fractional.len() + sign.len() + hashtag-prefix.len() + exponent-suffix.len()) if width-diff > 0 { // prefix with the appropriate amount of zeroes integral = ("0" * width-diff) + integral } } + // Format with thousands AFTER zeroes, but BEFORE applying textual prefixes + if fmt-thousands-separator != "" { + integral = str( + bytes( + array(bytes(integral.rev())) + .chunks(fmt-thousands-count) + .intersperse(array(bytes(fmt-thousands-separator.rev()))) + .join() + ) + ).rev() + } - replacement = integral + replaced-fractional + replacement = integral + replaced-fractional + exponent-suffix } else { sign = "" hashtag-prefix = "" @@ -502,8 +498,22 @@ parameter := argument '$' let named-replacements = replacements.named() let unnamed-format-index = 0 let fmt-decimal-separator = if "fmt-decimal-separator" in named-replacements { named-replacements.at("fmt-decimal-separator") } else { auto } + let fmt-thousands-count = if "fmt-thousands-count" in named-replacements { named-replacements.at("fmt-thousands-count") } else { 3 } let fmt-thousands-separator = if "fmt-thousands-separator" in named-replacements { named-replacements.at("fmt-thousands-separator") } else { "" } + assert( + type(fmt-thousands-count) == _int-type, + message: "String formatter error: 'fmt-thousands-count' must be an integer, got '" + type(fmt-thousands-count) + "' instead." + ) + assert( + fmt-thousands-count > 0, + message: "String formatter error: 'fmt-thousands-count' must be a positive integer, got " + str(fmt-thousands-count) + " instead." + ) + assert( + type(fmt-thousands-separator) == _str-type, + message: "String formatter error: 'fmt-thousands-separator' must be a string (or empty string, \"\", to disable), got '" + type(fmt-thousands-separator) + "' instead." + ) + let parts = () let last-span-end = 0 for f in formats { @@ -544,7 +554,7 @@ parameter := argument '$' } replace-by = named-replacements.at(name) } - replace-by = _generate-replacement(f.name, extras, replace-by, pos-replacements: num-replacements, named-replacements: named-replacements, fmt-decimal-separator: fmt-decimal-separator, fmt-thousands-separator: fmt-thousands-separator) + replace-by = _generate-replacement(f.name, extras, replace-by, pos-replacements: num-replacements, named-replacements: named-replacements, fmt-decimal-separator: fmt-decimal-separator, fmt-thousands-count: fmt-thousands-count, fmt-thousands-separator: fmt-thousands-separator) replace-span = f.span } else { panic("String formatter error: Internal error (unexpected format received).") diff --git a/tests/strfmt-tests.typ b/tests/strfmt-tests.typ index 27862c1..3ad76d7 100644 --- a/tests/strfmt-tests.typ +++ b/tests/strfmt-tests.typ @@ -57,6 +57,32 @@ // test grapheme clusters assert.eq(strfmt("Ĺo͂řȩ{}m̅", 5.5), "Ĺo͂řȩ5.5m̅") } +// Issue #5: Thousands +#{ + // Test separator + assert.eq(strfmt("{}", 10, fmt-thousands-separator: "_"), "10") + assert.eq(strfmt("{}", 1000, fmt-thousands-separator: ""), "1000") + assert.eq(strfmt("{}", 1000, fmt-thousands-separator: "_"), "1_000") + assert.eq(strfmt("{}", 100000000, fmt-thousands-separator: "_"), "100_000_000") + assert.eq(strfmt("{}", 10000000.3231, fmt-thousands-separator: "_"), "10_000_000.3231") + assert.eq(strfmt("{:010}", -23003, fmt-thousands-separator: "abc"), "-000abc023abc003") + assert.eq(strfmt("{:+013}", 23003.34, fmt-thousands-separator: "abc"), "+000abc023abc003.34") + assert.eq(strfmt("{:#b}", 255, fmt-thousands-separator: "_"), "0b11_111_111") + assert.eq(strfmt("{:#x}", -16 * 16 * 16 * 16 * 15, fmt-thousands-separator: "_"), "-0xf0_000") + assert.eq(strfmt("{:o}", -16 * 16 * 16 * 16 * 15, fmt-thousands-separator: "_"), "-3_600_000") + assert.eq(strfmt("{:e}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "heap"), "5heap5552e3") + assert.eq(strfmt("{:e}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "heap"), "5heap5552e3") + + // Test count + assert.eq(strfmt("{}", 10, fmt-thousands-count: 3, fmt-thousands-separator: "_"), "10") + assert.eq(strfmt("{}", 10, fmt-thousands-count: 1, fmt-thousands-separator: "_"), "1_0") + assert.eq(strfmt("{}", 1000, fmt-thousands-count: 2, fmt-thousands-separator: "_"), "10_00") + assert.eq(strfmt("{}", 10000000.3231, fmt-thousands-count: 2, fmt-thousands-separator: "_"), "10_00_00_00.3231") + assert.eq(strfmt("{:010}", -23003, fmt-thousands-count: 4, fmt-thousands-separator: "|"), "-0|0002|3003") + assert.eq(strfmt("{:#b}", 255, fmt-thousands-count: 1, fmt-thousands-separator: "_"), "0b1_1_1_1_1_1_1_1") + assert.eq(strfmt("{:#x}", -16 * 16 * 16 * 16 * 15, fmt-thousands-count: 2, fmt-thousands-separator: "_"), "-0xf_00_00") + assert.eq(strfmt("{:o}", -16 * 16 * 16 * 16 * 15, fmt-thousands-count: 4, fmt-thousands-separator: "_"), "-360_0000") +} // DOC TESTS #{ // --- Usage --- From 889b0f298d4b848373b9ae93ef3f617d2e38c091 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:39:31 -0300 Subject: [PATCH 03/18] bump minimum typst version --- .github/workflows/tests-ci.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index 3504f46..30ead9e 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -4,9 +4,9 @@ name: Tests CI on: # Triggers the workflow on push or pull request events but only for the branches below push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] # Allows one to run this workflow manually from the Actions tab workflow_dispatch: @@ -20,9 +20,9 @@ jobs: strategy: matrix: # Test for the following Typst versions - # 0.4.0 (earliest supported), 0.6.0 (first version with package management), + # 0.8.0 (earliest supported), 0.9.0 (first version with version checks), # 0.11.0 (latest supported) - typst-version: [v0.4.0, v0.6.0, v0.11.0] + typst-version: [v0.8.0, v0.9.0, v0.11.0] # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -35,10 +35,5 @@ jobs: with: typst-version: ${{ matrix.typst-version }} - - name: 🛠️ Compile test document ( Date: Tue, 1 Oct 2024 00:43:42 -0300 Subject: [PATCH 04/18] test version compat --- oxifmt.typ | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/oxifmt.typ b/oxifmt.typ index 49e4dc9..2aeea5c 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -6,6 +6,11 @@ #let _str-type = type("") #let _label-type = type() +#let using-080 = type(type(5)) != _str-type +#let using-090 = using-080 and str(-1).codepoints().first() == "\u{2212}" + +#panic(using-090) + #let _strfmt_formatparser(s) = { if type(s) != _str-type { panic("String format parsing internal error: String format parser given non-string.") From da06f9dec2ef239cef9cc46e7c0c31a7619a75d6 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:43:42 -0300 Subject: [PATCH 05/18] polyfill array chunks --- oxifmt.typ | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index 2aeea5c..28365c2 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -9,7 +9,22 @@ #let using-080 = type(type(5)) != _str-type #let using-090 = using-080 and str(-1).codepoints().first() == "\u{2212}" -#panic(using-090) +#let _arr-chunks = if using-090 { + array.chunks +} else { + (arr, chunks) => { + let i = 0 + let res = () + for element in arr { + if calc.rem(i, chunks) == 0 { + res.push(()) + } + res.last().push(element) + i += 1 + } + res + } +} #let _strfmt_formatparser(s) = { if type(s) != _str-type { @@ -265,10 +280,12 @@ parameter := argument '$' if fmt-thousands-separator != "" { integral = str( bytes( - array(bytes(integral.rev())) - .chunks(fmt-thousands-count) - .intersperse(array(bytes(fmt-thousands-separator.rev()))) - .join() + _arr-chunks( + array(bytes(integral.rev())), + fmt-thousands-count + ) + .intersperse(array(bytes(fmt-thousands-separator.rev()))) + .join() ) ).rev() } @@ -445,10 +462,12 @@ parameter := argument '$' if fmt-thousands-separator != "" { integral = str( bytes( - array(bytes(integral.rev())) - .chunks(fmt-thousands-count) - .intersperse(array(bytes(fmt-thousands-separator.rev()))) - .join() + _arr-chunks( + array(bytes(integral.rev())), + fmt-thousands-count + ) + .intersperse(array(bytes(fmt-thousands-separator.rev()))) + .join() ) ).rev() } From f58c8bde6091e1e0ee2d8bbf4834ddb3e482e85a Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:04:43 -0300 Subject: [PATCH 06/18] fix polyfill version --- oxifmt.typ | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oxifmt.typ b/oxifmt.typ index 28365c2..280d7bf 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -8,8 +8,9 @@ #let using-080 = type(type(5)) != _str-type #let using-090 = using-080 and str(-1).codepoints().first() == "\u{2212}" +#let using-0110 = using-090 and sys.version >= version(0, 11, 0) -#let _arr-chunks = if using-090 { +#let _arr-chunks = if using-0110 { array.chunks } else { (arr, chunks) => { From 98af1a7a853585ef8fd579380626f6163bc3db2a Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:06:35 -0300 Subject: [PATCH 07/18] add Typst v0.10.0 to tests --- .github/workflows/tests-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index 30ead9e..2f3fa70 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -21,8 +21,8 @@ jobs: matrix: # Test for the following Typst versions # 0.8.0 (earliest supported), 0.9.0 (first version with version checks), - # 0.11.0 (latest supported) - typst-version: [v0.8.0, v0.9.0, v0.11.0] + # 0.10.0, 0.11.0 (latest supported) + typst-version: [v0.8.0, v0.9.0, v0.10.0, v0.11.0] # Steps represent a sequence of tasks that will be executed as part of the job steps: From 3efb6a61722ffa37ff121bf601757d10787d2209 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:20:56 -0300 Subject: [PATCH 08/18] workaround for repr inconsistency --- oxifmt.typ | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/oxifmt.typ b/oxifmt.typ index 280d7bf..f9b9b88 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -441,7 +441,13 @@ parameter := argument '$' } else { precision = none let new-replacement = if spectype.ends-with("?") { - repr(replacement) + let repr-res = repr(replacement) + if using-090 and not using-0110 and type(replacement) == _float-type and "." not in repr-res { + // Workaround for repr inconsistency in Typst 0.9.0 and 0.10.0 + repr-res + ".0" + } else { + repr-res + } } else { _strfmt_stringify(replacement) } From beef6adef800cf612b0de06f82c149f32f51722e Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:24:45 -0300 Subject: [PATCH 09/18] fix utf-8 bugs --- oxifmt.typ | 4 ++-- tests/strfmt-tests.typ | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index f9b9b88..ccbaec1 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -459,7 +459,7 @@ parameter := argument '$' let exponent-suffix = exponent-suffix.replace(".", decimal-separator) if zero { - let width-diff = width - (integral.len() + replaced-fractional.len() + sign.len() + hashtag-prefix.len() + exponent-suffix.len()) + let width-diff = width - (integral.len() + replaced-fractional.codepoints().len() + sign.len() + hashtag-prefix.len() + exponent-suffix.codepoints().len()) if width-diff > 0 { // prefix with the appropriate amount of zeroes integral = ("0" * width-diff) + integral } @@ -506,7 +506,7 @@ parameter := argument '$' if fill != none { // perform fill/width adjustments: "x" ---> " x" if width is 4 - let width-diff = width - replacement.len() // number prefixes are also considered for width + let width-diff = width - replacement.codepoints().len() // number prefixes are also considered for width if width-diff > 0 { if align == left { replacement = replacement + (fill * width-diff) diff --git a/tests/strfmt-tests.typ b/tests/strfmt-tests.typ index 3ad76d7..61152c3 100644 --- a/tests/strfmt-tests.typ +++ b/tests/strfmt-tests.typ @@ -56,6 +56,10 @@ // test grapheme clusters assert.eq(strfmt("Ĺo͂řȩ{}m̅", 5.5), "Ĺo͂řȩ5.5m̅") + + // padding should use codepoint len + assert.eq(strfmt("{:€>15}", "Ĺo͂řȩ5.5m̅"), "€€€€€Ĺo͂řȩ5.5m̅") + assert.eq(strfmt("{:€>10}", "abc€d"), "€€€€€abc€d") } // Issue #5: Thousands #{ @@ -64,14 +68,18 @@ assert.eq(strfmt("{}", 1000, fmt-thousands-separator: ""), "1000") assert.eq(strfmt("{}", 1000, fmt-thousands-separator: "_"), "1_000") assert.eq(strfmt("{}", 100000000, fmt-thousands-separator: "_"), "100_000_000") + assert.eq(strfmt("{}", 100000000.0, fmt-thousands-separator: "_"), "100_000_000") assert.eq(strfmt("{}", 10000000.3231, fmt-thousands-separator: "_"), "10_000_000.3231") assert.eq(strfmt("{:010}", -23003, fmt-thousands-separator: "abc"), "-000abc023abc003") assert.eq(strfmt("{:+013}", 23003.34, fmt-thousands-separator: "abc"), "+000abc023abc003.34") assert.eq(strfmt("{:#b}", 255, fmt-thousands-separator: "_"), "0b11_111_111") assert.eq(strfmt("{:#x}", -16 * 16 * 16 * 16 * 15, fmt-thousands-separator: "_"), "-0xf0_000") assert.eq(strfmt("{:o}", -16 * 16 * 16 * 16 * 15, fmt-thousands-separator: "_"), "-3_600_000") + assert.eq(strfmt("{:?}", 5555.0, fmt-thousands-separator: "_"), "5_555.0") assert.eq(strfmt("{:e}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "heap"), "5heap5552e3") - assert.eq(strfmt("{:e}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "heap"), "5heap5552e3") + assert.eq(strfmt("{:010}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "€"), "00_005_555€2") + assert.eq(strfmt("{:€>10}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "€"), "€€€5_555€2") + assert.eq(strfmt("{:€>10}", 5555.2, fmt-thousands-separator: "€", fmt-decimal-separator: "€"), "€€€5€555€2") // Test count assert.eq(strfmt("{}", 10, fmt-thousands-count: 3, fmt-thousands-separator: "_"), "10") From 777579373115a6d7ede2f04f34d89e20777ad982 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:45:39 -0300 Subject: [PATCH 10/18] fix simple negative numbers --- oxifmt.typ | 9 +++++---- tests/strfmt-tests.typ | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index ccbaec1..d996448 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -274,9 +274,10 @@ parameter := argument '$' ) = { if extras == none { let is-numeric = _strfmt_is-numeric-type(replacement) - let string-replacement = _strfmt_stringify(replacement) if is-numeric { + let string-replacement = _strfmt_stringify(calc.abs(replacement)) + let sign = if replacement < 0 { "-" } else { "" } let (integral, ..fractional) = string-replacement.split(".") if fmt-thousands-separator != "" { integral = str( @@ -293,12 +294,12 @@ parameter := argument '$' if fractional.len() > 0 { let decimal-separator = if fmt-decimal-separator not in (auto, none) { _strfmt_stringify(fmt-decimal-separator) } else { "." } - return integral + decimal-separator + fractional.first() + return sign + integral + decimal-separator + fractional.first() } else { - return integral + return sign + integral } } else { - return string-replacement + return _strfmt_stringify(replacement) } } let extras = _strfmt_stringify(extras) diff --git a/tests/strfmt-tests.typ b/tests/strfmt-tests.typ index 61152c3..7006ac9 100644 --- a/tests/strfmt-tests.typ +++ b/tests/strfmt-tests.typ @@ -70,6 +70,9 @@ assert.eq(strfmt("{}", 100000000, fmt-thousands-separator: "_"), "100_000_000") assert.eq(strfmt("{}", 100000000.0, fmt-thousands-separator: "_"), "100_000_000") assert.eq(strfmt("{}", 10000000.3231, fmt-thousands-separator: "_"), "10_000_000.3231") + assert.eq(strfmt("{}", -230, fmt-thousands-separator: "_"), "-230") + assert.eq(strfmt("{}", -2300, fmt-thousands-separator: "_"), "-2_300") + assert.eq(strfmt("{}", -2300.453, fmt-thousands-separator: "_"), "-2_300.453") assert.eq(strfmt("{:010}", -23003, fmt-thousands-separator: "abc"), "-000abc023abc003") assert.eq(strfmt("{:+013}", 23003.34, fmt-thousands-separator: "abc"), "+000abc023abc003.34") assert.eq(strfmt("{:#b}", 255, fmt-thousands-separator: "_"), "0b11_111_111") From 424186df3b7e3b32162198865ae513335b1807e2 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:22:53 -0300 Subject: [PATCH 11/18] use array.rev() for thousands separator stuff --- oxifmt.typ | 16 ++++++++-------- tests/strfmt-tests.typ | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index d996448..0f01f51 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -283,13 +283,13 @@ parameter := argument '$' integral = str( bytes( _arr-chunks( - array(bytes(integral.rev())), + array(bytes(integral)).rev(), fmt-thousands-count ) - .intersperse(array(bytes(fmt-thousands-separator.rev()))) - .join() + .join(array(bytes(fmt-thousands-separator)).rev()) + .rev() ) - ).rev() + ) } if fractional.len() > 0 { @@ -471,13 +471,13 @@ parameter := argument '$' integral = str( bytes( _arr-chunks( - array(bytes(integral.rev())), + array(bytes(integral)).rev(), fmt-thousands-count ) - .intersperse(array(bytes(fmt-thousands-separator.rev()))) - .join() + .join(array(bytes(fmt-thousands-separator)).rev()) + .rev() ) - ).rev() + ) } replacement = integral + replaced-fractional + exponent-suffix diff --git a/tests/strfmt-tests.typ b/tests/strfmt-tests.typ index 7006ac9..71b53ee 100644 --- a/tests/strfmt-tests.typ +++ b/tests/strfmt-tests.typ @@ -73,6 +73,7 @@ assert.eq(strfmt("{}", -230, fmt-thousands-separator: "_"), "-230") assert.eq(strfmt("{}", -2300, fmt-thousands-separator: "_"), "-2_300") assert.eq(strfmt("{}", -2300.453, fmt-thousands-separator: "_"), "-2_300.453") + assert.eq(strfmt("{}", 5555.2, fmt-thousands-separator: "€", fmt-decimal-separator: "€€"), "5€555€€2") assert.eq(strfmt("{:010}", -23003, fmt-thousands-separator: "abc"), "-000abc023abc003") assert.eq(strfmt("{:+013}", 23003.34, fmt-thousands-separator: "abc"), "+000abc023abc003.34") assert.eq(strfmt("{:#b}", 255, fmt-thousands-separator: "_"), "0b11_111_111") From e1a28015b2ccec18c7a307f6c49ac30336fc6213 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:22:53 -0300 Subject: [PATCH 12/18] handle NaN --- oxifmt.typ | 26 ++++++++++++++++++++------ tests/strfmt-tests.typ | 2 ++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index 0f01f51..8c2908b 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -27,6 +27,18 @@ } } +#let _float-is-nan = if using-0110 { + float.is-nan +} else { + x => type(x) == _float-type and "NaN" in repr(x) +} + +#let _float-is-infinite = if using-0110 { + float.is-infinite +} else { + x => type(x) == _float-type and "inf" in repr(x) +} + #let _strfmt_formatparser(s) = { if type(s) != _str-type { panic("String format parsing internal error: String format parser given non-string.") @@ -216,7 +228,7 @@ } #let _strfmt_with-precision(num, precision) = { - if precision == none { + if precision == none or type(num) == _float-type and (_float-is-nan(num) or _float-is-infinite(num)) { return _strfmt_stringify(num) } let result = _strfmt_stringify(calc.round(float(num), digits: calc.min(50, precision))) @@ -274,12 +286,13 @@ parameter := argument '$' ) = { if extras == none { let is-numeric = _strfmt_is-numeric-type(replacement) + let is-nan = type(replacement) == _float-type and _float-is-nan(replacement) if is-numeric { let string-replacement = _strfmt_stringify(calc.abs(replacement)) - let sign = if replacement < 0 { "-" } else { "" } + let sign = if not is-nan and replacement < 0 { "-" } else { "" } let (integral, ..fractional) = string-replacement.split(".") - if fmt-thousands-separator != "" { + if fmt-thousands-separator != "" and (type(replacement) != _float-type or not _float-is-nan(replacement) and not _float-is-infinite(replacement)) { integral = str( bytes( _arr-chunks( @@ -392,6 +405,7 @@ parameter := argument '$' let is-numeric = _strfmt_is-numeric-type(replacement) if is-numeric { + let is-nan = type(replacement) == _float-type and _float-is-nan(replacement) if zero { // disable fill, we will be prefixing with zeroes if necessary fill = none @@ -405,9 +419,9 @@ parameter := argument '$' } // if + is specified, + will appear before all numbers >= 0. - if sign == "+" and replacement >= 0 { + if sign == "+" and not is-nan and replacement >= 0 { sign = "+" - } else if replacement < 0 { + } else if not is-nan and replacement < 0 { sign = "-" } else { sign = "" @@ -467,7 +481,7 @@ parameter := argument '$' } // Format with thousands AFTER zeroes, but BEFORE applying textual prefixes - if fmt-thousands-separator != "" { + if fmt-thousands-separator != "" and (type(replacement) != _float-type or not _float-is-nan(replacement) and not _float-is-infinite(replacement)) { integral = str( bytes( _arr-chunks( diff --git a/tests/strfmt-tests.typ b/tests/strfmt-tests.typ index 71b53ee..5c2ba58 100644 --- a/tests/strfmt-tests.typ +++ b/tests/strfmt-tests.typ @@ -90,10 +90,12 @@ assert.eq(strfmt("{}", 10, fmt-thousands-count: 1, fmt-thousands-separator: "_"), "1_0") assert.eq(strfmt("{}", 1000, fmt-thousands-count: 2, fmt-thousands-separator: "_"), "10_00") assert.eq(strfmt("{}", 10000000.3231, fmt-thousands-count: 2, fmt-thousands-separator: "_"), "10_00_00_00.3231") + assert.eq(strfmt("{}", float("nan"), fmt-thousands-count: 2, fmt-thousands-separator: "_"), "NaN") assert.eq(strfmt("{:010}", -23003, fmt-thousands-count: 4, fmt-thousands-separator: "|"), "-0|0002|3003") assert.eq(strfmt("{:#b}", 255, fmt-thousands-count: 1, fmt-thousands-separator: "_"), "0b1_1_1_1_1_1_1_1") assert.eq(strfmt("{:#x}", -16 * 16 * 16 * 16 * 15, fmt-thousands-count: 2, fmt-thousands-separator: "_"), "-0xf_00_00") assert.eq(strfmt("{:o}", -16 * 16 * 16 * 16 * 15, fmt-thousands-count: 4, fmt-thousands-separator: "_"), "-360_0000") + assert.eq(strfmt("{:05}", float("nan"), fmt-thousands-count: 2, fmt-thousands-separator: "_"), "00NaN") } // DOC TESTS #{ From 282b3a0fc3c6c591d1a8a5ffc30f4a079cd1f727 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:22:53 -0300 Subject: [PATCH 13/18] update this test --- tests/strfmt-tests.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strfmt-tests.typ b/tests/strfmt-tests.typ index 5c2ba58..03d8309 100644 --- a/tests/strfmt-tests.typ +++ b/tests/strfmt-tests.typ @@ -83,7 +83,7 @@ assert.eq(strfmt("{:e}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "heap"), "5heap5552e3") assert.eq(strfmt("{:010}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "€"), "00_005_555€2") assert.eq(strfmt("{:€>10}", 5555.2, fmt-thousands-separator: "_", fmt-decimal-separator: "€"), "€€€5_555€2") - assert.eq(strfmt("{:€>10}", 5555.2, fmt-thousands-separator: "€", fmt-decimal-separator: "€"), "€€€5€555€2") + assert.eq(strfmt("{:€>10}", 5555.2, fmt-thousands-separator: "€a", fmt-decimal-separator: "€"), "€€5€a555€2") // Test count assert.eq(strfmt("{}", 10, fmt-thousands-count: 3, fmt-thousands-separator: "_"), "10") From 0dcac02114e9d18c4faaca9ed9bcf91538eb2f23 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:40:44 -0300 Subject: [PATCH 14/18] allow typst 0.7.0 --- .github/workflows/tests-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index 2f3fa70..fa44cf3 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -20,9 +20,10 @@ jobs: strategy: matrix: # Test for the following Typst versions - # 0.8.0 (earliest supported), 0.9.0 (first version with version checks), - # 0.10.0, 0.11.0 (latest supported) - typst-version: [v0.8.0, v0.9.0, v0.10.0, v0.11.0] + # 0.7.0 (earliest supported), 0.8.0, + # 0.9.0 (first version with version checks), 0.10.0, + # 0.11.0 (latest supported) + typst-version: [v0.7.0, v0.8.0, v0.9.0, v0.10.0, v0.11.0] # Steps represent a sequence of tasks that will be executed as part of the job steps: From acd9efa63df31e7789c76780d7b9576d9679a2de Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:43:40 -0300 Subject: [PATCH 15/18] use codepoints for thousand sep --- oxifmt.typ | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index 8c2908b..3a0c198 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -293,16 +293,10 @@ parameter := argument '$' let sign = if not is-nan and replacement < 0 { "-" } else { "" } let (integral, ..fractional) = string-replacement.split(".") if fmt-thousands-separator != "" and (type(replacement) != _float-type or not _float-is-nan(replacement) and not _float-is-infinite(replacement)) { - integral = str( - bytes( - _arr-chunks( - array(bytes(integral)).rev(), - fmt-thousands-count - ) - .join(array(bytes(fmt-thousands-separator)).rev()) - .rev() - ) - ) + integral = _arr-chunks(integral.codepoints().rev(), fmt-thousands-count) + .join(fmt-thousands-separator.codepoints().rev()) + .rev() + .join() } if fractional.len() > 0 { @@ -482,16 +476,10 @@ parameter := argument '$' // Format with thousands AFTER zeroes, but BEFORE applying textual prefixes if fmt-thousands-separator != "" and (type(replacement) != _float-type or not _float-is-nan(replacement) and not _float-is-infinite(replacement)) { - integral = str( - bytes( - _arr-chunks( - array(bytes(integral)).rev(), - fmt-thousands-count - ) - .join(array(bytes(fmt-thousands-separator)).rev()) - .rev() - ) - ) + integral = _arr-chunks(integral.codepoints().rev(), fmt-thousands-count) + .join(fmt-thousands-separator.codepoints().rev()) + .rev() + .join() } replacement = integral + replaced-fractional + exponent-suffix From b20502833c6e469ed4b6e0750f6d2cef22403fb2 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:23:32 -0300 Subject: [PATCH 16/18] optimize arr-chunks polyfill because why not --- oxifmt.typ | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index 3a0c198..0a2b50d 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -17,11 +17,12 @@ let i = 0 let res = () for element in arr { - if calc.rem(i, chunks) == 0 { + if i == 0 { res.push(()) + i = chunks } res.last().push(element) - i += 1 + i -= 1 } res } From 63b25c0ef53c4f2c4ae9bfcacb917bfaa56518f9 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:28:03 -0300 Subject: [PATCH 17/18] test more recent typst versions --- .github/workflows/tests-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index fa44cf3..b4cdf8a 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -22,8 +22,9 @@ jobs: # Test for the following Typst versions # 0.7.0 (earliest supported), 0.8.0, # 0.9.0 (first version with version checks), 0.10.0, - # 0.11.0 (latest supported) - typst-version: [v0.7.0, v0.8.0, v0.9.0, v0.10.0, v0.11.0] + # 0.11.0, 0.11.1, 0.12-rc1 (latest supported) + typst-version: + [v0.7.0, v0.8.0, v0.9.0, v0.10.0, v0.11.0, v0.11.1, v0.12.0-rc1] # Steps represent a sequence of tasks that will be executed as part of the job steps: From e51c4d4b0fc5cbfd911c6ea2dcdbf7389e58b81b Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:28:03 -0300 Subject: [PATCH 18/18] move is-nan --- oxifmt.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oxifmt.typ b/oxifmt.typ index 0a2b50d..4ca2c66 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -287,9 +287,9 @@ parameter := argument '$' ) = { if extras == none { let is-numeric = _strfmt_is-numeric-type(replacement) - let is-nan = type(replacement) == _float-type and _float-is-nan(replacement) if is-numeric { + let is-nan = type(replacement) == _float-type and _float-is-nan(replacement) let string-replacement = _strfmt_stringify(calc.abs(replacement)) let sign = if not is-nan and replacement < 0 { "-" } else { "" } let (integral, ..fractional) = string-replacement.split(".")