feat: minus sign and number formatting

This commit is contained in:
2026-07-07 01:26:09 -04:00
parent 48bced3f4c
commit c5fd2c4c0e
+66 -3
View File
@@ -1,6 +1,45 @@
local NBSP = "\u{00A0}" local NBSP = "\u{00A0}"
local function nbsp() return pandoc.Str(NBSP) end 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 -- render a value already known to be in scientific-notation range
-- returns a list of inlines: mantissa <NBSP> × <NBSP> 10^exp^ -- returns a list of inlines: mantissa <NBSP> × <NBSP> 10^exp^
local function formatSci(value) local function formatSci(value)
@@ -14,13 +53,13 @@ local function formatSci(value)
mantissa = mantissa:gsub("0+$", ""):gsub("%.$", "") mantissa = mantissa:gsub("0+$", ""):gsub("%.$", "")
local expNum = tonumber(sign .. exp) -- keeps sign, drops leading zeros (e.g. "+23" -> 23, "-06" -> -6) 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 if expNum ~= 0 then
table.insert(out, nbsp()) table.insert(out, nbsp())
table.insert(out, pandoc.Str("×")) table.insert(out, pandoc.Str("×"))
table.insert(out, nbsp()) table.insert(out, nbsp())
table.insert(out, pandoc.Str("10")) 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 end
return out return out
end end
@@ -38,7 +77,14 @@ local function formatPlain(value)
if s:find("[eE]") then if s:find("[eE]") then
return formatSci(value) return formatSci(value)
end 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 end
-- decide plain vs. scientific, unless overridden. -- decide plain vs. scientific, unless overridden.
@@ -163,6 +209,22 @@ local function sci(args)
return formatSci(value) return formatSci(value)
end 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. -- generic converter, parameterized by which table to use.
-- optional kwargs.notation = "sci" | "plain" forces that format -- optional kwargs.notation = "sci" | "plain" forces that format
-- for both the source value and (if present) the converted value. -- for both the source value and (if present) the converted value.
@@ -282,6 +344,7 @@ end
return { return {
["unit"] = unit, ["unit"] = unit,
["sci"] = sci, ["sci"] = sci,
["number"] = number,
["energy"] = makeQuantityShortcode(ENERGY_UNITS, "energy"), ["energy"] = makeQuantityShortcode(ENERGY_UNITS, "energy"),
["molarenergy"] = makeQuantityShortcode(MOLAR_ENERGY_UNITS, "molarenergy"), ["molarenergy"] = makeQuantityShortcode(MOLAR_ENERGY_UNITS, "molarenergy"),
["distance"] = makeQuantityShortcode(DISTANCE_UNITS, "distance"), ["distance"] = makeQuantityShortcode(DISTANCE_UNITS, "distance"),