From c5fd2c4c0ee0e9f057c8d31ad529dca908b0427d Mon Sep 17 00:00:00 2001 From: Alex Maldonado Date: Tue, 7 Jul 2026 01:26:09 -0400 Subject: [PATCH] feat: minus sign and number formatting --- _extensions/units/units.lua | 69 +++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/_extensions/units/units.lua b/_extensions/units/units.lua index 0e4ccf7..1180506 100644 --- a/_extensions/units/units.lua +++ b/_extensions/units/units.lua @@ -1,6 +1,45 @@ local NBSP = "\u{00A0}" local function nbsp() return pandoc.Str(NBSP) end +-- The true minus sign, U+2212 -- NOT the ASCII hyphen-minus (U+002D) that +-- string.format emits by default, and NOT an en dash (U+2013, reserved for +-- ranges like "10-20 ns"). U+2212 is metrically designed to match the +-- width and vertical centering of "+" (U+002B), so signed values align +-- visually the way they would in typeset mathematics. This is applied +-- only at the final display step -- internal parsing below still matches +-- against the plain ASCII "-" that Lua's own %g/%e formatting produces. +local MINUS = "\u{2212}" +local function displayMinus(s) + return (s:gsub("^%-", MINUS)) +end + +-- Non-breaking DIGIT-GROUPING separator, per the SI Brochure / NIST SP811 / +-- IUPAC Green Book convention: group digits in threes using a thin space +-- (never a comma or period, since those mean different things -- decimal +-- vs. thousands separator -- depending on locale). U+202F NARROW NO-BREAK +-- SPACE is used rather than U+2009 THIN SPACE because the latter is an +-- ordinary breakable space and could split a number across a line wrap. +local THIN_NBSP = "\u{202F}" + +-- group a string of digits (no sign, no decimal point) into threes, +-- e.g. "12345" -> "12" THIN_NBSP "345". Per NIST SP811, grouping is +-- optional for exactly four digits and left ungrouped here; it is +-- effectively mandatory from five digits up, since that's the point at +-- which an ungrouped run of digits becomes hard to parse at a glance. +local function groupDigits(digits) + local len = #digits + if len <= 4 then return digits end + local firstLen = len % 3 + if firstLen == 0 then firstLen = 3 end + local groups = { digits:sub(1, firstLen) } + local i = firstLen + 1 + while i <= len do + table.insert(groups, digits:sub(i, i + 2)) + i = i + 3 + end + return table.concat(groups, THIN_NBSP) +end + -- render a value already known to be in scientific-notation range -- returns a list of inlines: mantissa × 10^exp^ local function formatSci(value) @@ -14,13 +53,13 @@ local function formatSci(value) mantissa = mantissa:gsub("0+$", ""):gsub("%.$", "") local expNum = tonumber(sign .. exp) -- keeps sign, drops leading zeros (e.g. "+23" -> 23, "-06" -> -6) - local out = { pandoc.Str(mantissa) } + local out = { pandoc.Str(displayMinus(mantissa)) } if expNum ~= 0 then table.insert(out, nbsp()) table.insert(out, pandoc.Str("×")) table.insert(out, nbsp()) table.insert(out, pandoc.Str("10")) - table.insert(out, pandoc.Superscript({ pandoc.Str(tostring(expNum)) })) + table.insert(out, pandoc.Superscript({ pandoc.Str(displayMinus(tostring(expNum))) })) end return out end @@ -38,7 +77,14 @@ local function formatPlain(value) if s:find("[eE]") then return formatSci(value) end - return { pandoc.Str(s) } + -- split into optional sign, integer part, and remainder (decimal point + -- + fractional digits, if any) so grouping applies only to the + -- integer part -- fractional digits are never grouped + local sign, intPart, rest = s:match("^(%-?)(%d+)(.*)$") + if intPart then + s = sign .. groupDigits(intPart) .. rest + end + return { pandoc.Str(displayMinus(s)) } end -- decide plain vs. scientific, unless overridden. @@ -163,6 +209,22 @@ local function sci(args) return formatSci(value) end +-- {{< number 12345 >}} -> 12 345 (auto plain/sci, grouped) +-- {{< number 12345 notation=plain >}} -> 12 345 (forced plain, still grouped) +-- {{< number 12345 notation=sci >}} -> 1.23 × 10⁴ +-- Bare-number equivalent of the unit shortcodes above, for cases where a +-- value has no associated unit but should still receive the same digit +-- grouping and notation handling. +local function number(args, kwargs) + local value = tonumber(pandoc.utils.stringify(args[1])) + local mode = kwargs and kwargs["notation"] and pandoc.utils.stringify(kwargs["notation"]) or nil + if not value then + io.stderr:write("[units] bad number() arg: '" .. pandoc.utils.stringify(args[1]) .. "'\n") + return pandoc.Str("[number error]") + end + return formatNumber(value, mode) +end + -- generic converter, parameterized by which table to use. -- optional kwargs.notation = "sci" | "plain" forces that format -- for both the source value and (if present) the converted value. @@ -282,6 +344,7 @@ end return { ["unit"] = unit, ["sci"] = sci, + ["number"] = number, ["energy"] = makeQuantityShortcode(ENERGY_UNITS, "energy"), ["molarenergy"] = makeQuantityShortcode(MOLAR_ENERGY_UNITS, "molarenergy"), ["distance"] = makeQuantityShortcode(DISTANCE_UNITS, "distance"),