diff --git a/_extensions/units/units.lua b/_extensions/units/units.lua index 1180506..9d1d17d 100644 --- a/_extensions/units/units.lua +++ b/_extensions/units/units.lua @@ -64,35 +64,66 @@ local function formatSci(value) return out end +local function finalizePlainString(s) + local sign, intPart, rest = s:match("^(%-?)(%d+)(.*)$") + if intPart then + s = sign .. groupDigits(intPart) .. rest + end + return displayMinus(s) +end + -- render a value in plain decimal notation, 3 significant figures. -- IMPORTANT: C's %g spec has its own internal rule for switching to -- exponential form ("use %e if exponent < -4 or exponent >= precision"). -- With precision 3, that means %.3g silently flips to "1e+03"-style -- output for anything >= 1000 -- BEFORE any magnitude threshold of ours --- gets a say. Rather than duplicating that rule with a second threshold +-- gets a say. Rather than duplicating that rule with a second threshold, -- we let %g decide, then intercept its exponential output and re-render it through -- formatSci() instead of ever emitting the raw "1e+03" string. +-- NOTE: this function is used for the "auto" mode ONLY. It is allowed to +-- fall back to scientific notation. For an explicit, non-negotiable +-- notation="plain" override, see formatForcedPlain below instead. local function formatPlain(value) local s = string.format("%.3g", value) if s:find("[eE]") then return formatSci(value) end - -- 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 + return { pandoc.Str(finalizePlainString(s)) } +end + +-- render a value in plain decimal notation that NEVER falls back to +-- scientific form, regardless of magnitude, this is what +-- notation="plain" actually promises the caller. %g's own exponent-switch +-- rule (the reason formatPlain above sometimes defers to formatSci) is +-- sidestepped entirely by computing the required number of decimal places +-- directly from the value's exponent (read robustly off a %e string, +-- since %e always reports the exponent Lua/C computed rather than +-- re-deriving it ourselves and risking a boundary rounding mismatch) and +-- formatting with %f at that precision. +local function formatForcedPlain(value, sig) + sig = sig or 3 + if value == 0 then return { pandoc.Str("0") } end + local sciStr = string.format("%." .. (sig - 1) .. "e", value) + local exp = tonumber(sciStr:match("e([+-]?%d+)$")) + local decimals = sig - 1 - exp + if decimals < 0 then decimals = 0 end + local s = string.format("%." .. decimals .. "f", value) + if s:find("%.") then + s = s:gsub("0+$", ""):gsub("%.$", "") end - return { pandoc.Str(displayMinus(s)) } + return { pandoc.Str(finalizePlainString(s)) } end -- decide plain vs. scientific, unless overridden. -- mode: nil/"auto" (let %g decide, see formatPlain), "sci", or "plain" +-- "plain" is a hard override -- it must never silently become scientific, +-- which is exactly the bug that motivated splitting formatForcedPlain out +-- from formatPlain rather than reusing it for both cases. local function formatNumber(value, mode) if value == 0 then return { pandoc.Str("0") } end if mode == "sci" then return formatSci(value) end - return formatPlain(value) -- handles both "auto" and explicit "plain" + if mode == "plain" then return formatForcedPlain(value) end + return formatPlain(value) -- "auto" end local function appendAll(dst, list)