Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Zero's core is the `num()` function, which provides flexible number formatting.
#num(
number: str | content | int | float | dictionary | array,
digits: auto | int = auto,
fixed: none | int = none,
exponent: auto | str = auto,

decimal-separator: str = ".",
product: content = sym.times,
Expand All @@ -116,7 +116,12 @@ Zero's core is the `num()` function, which provides flexible number formatting.
```
- `number: str | content | int | float | array` : Number input; `str` is preferred. If the input is `content`, it may only contain text nodes. Numeric types `int` and `float` are supported but not encouraged because of information loss (e.g., the number of trailing "0" digits or the exponent). The remaining types `dictionary` and `array` are intended for advanced use, see [below](#zero-for-third-party-packages).
- `digits: auto | int = auto` : Truncates the number at a given (positive) number of decimal places or pads the number with zeros if necessary. This is independent of [rounding](#rounding).
- `fixed: none | int = none` : If not `none`, forces a fixed exponent. Additional exponents given in the number input are taken into account.
- `exponent: auto | str = auto` : Controls the value of the exponent. Possible values are
- `auto` : The exponent is taken to be the _input exponent_, e.g., `5` for `num[1e5]`.
- `"sci"` : The exponent is chosen according to scientific notation. Use `(sci: n)` to activate scientific notation only when the absolute of the exponent is at least `n` or `(sci: (min, max))` to activate scientific notation only when the exponent is less or equal to `min` or greater or equal to `max`.
- `"eng"` : The exponent chosen according to engineering notation, that is the exponent is a multiple of three and the integer part is never zero.
- `(fixed: n)` : The exponent is fixed to the given integer.

- `decimal-separator: str = "."` : Specifies the marker that is used for separating integer and decimal part.
- `product: content = sym.times` : Specifies the multiplication symbol used for scientific notation.
- `tight: bool = false` : If true, tight spacing is applied between operands (applies to $\times$ and $\pm$).
Expand Down Expand Up @@ -399,7 +404,8 @@ The appearance of units can be configured via `set-unit`:
#set-unit(
unit-separator: content = sym.space.thin,
fraction: str = "power",
breakable: bool = false
breakable: bool = false,
prefix auto | none = auto
)
```
- `unit-separator: content` : Configures the separator between consecutive unit parts in a composite unit.
Expand All @@ -408,6 +414,7 @@ The appearance of units can be configured via `set-unit`:
- `"fraction"` : When units with negative exponents are present, a fraction is created and the concerned units are put in the denominator.
- `"inline"` : An inline fraction is created.
- `breakable: bool` : Whether units and quantities can be broken across paragraph lines.
- `prefix: auto` : When set to `auto` and `num.exponent` is set to `"eng"`, a metric prefix is displayed along with the unit, replacing the exponent, e.g. `zi.m[2e4]` will render as 20km.

These options are also available when instancing a quantity, e.g., `#zi.m(fraction: "inline")[2.5]`.

Expand Down
12 changes: 12 additions & 0 deletions src/assertations.typ
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,15 @@
assert(false, message: message)
}
}


#let assert-no-fixed(args) = {
return
if "fixed" in args.named() {
let value = str(args.at("fixed"))
assert(
false,
message: "The parameter `fixed` has been removed. Instead use `exponent (fixed: " + value + "`) instead of `fixed: " + value + "`"
)
}
}
14 changes: 5 additions & 9 deletions src/formatting.typ
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,11 @@
/// if `positive-sign` is set to true. In all other cases, the result is
/// `none`.
#let format-sign(sign, positive-sign: false) = {
if sign == "-" { return "−" }
else if sign == "+" and positive-sign { return "+" }
if sign == "-" { return math.class("unary", sym.minus) }
else if sign == "+" and positive-sign { return math.class("unary", sym.plus) }
}

#assert.eq(format-sign("-", positive-sign: false), "−")
#assert.eq(format-sign("+", positive-sign: false), none)
#assert.eq(format-sign("-", positive-sign: true), "−")
#assert.eq(format-sign("+", positive-sign: true), "+")
#assert.eq(format-sign(none, positive-sign: true), none)




Expand Down Expand Up @@ -163,7 +159,7 @@
)

if compact-pm {
pm = pm.map(x => utility.shift-decimal-left(..x, -it.digits))
pm = pm.map(x => utility.shift-decimal-left(..x, digits: -it.digits))
it.digits = auto
}
}
Expand Down Expand Up @@ -215,7 +211,7 @@

let (sign, integer, fractional) = decompose-signed-float-string(it.exponent)
let exponent = format-comma-number((sign: sign, int: integer, frac: fractional, digits: auto, group: false, positive-sign: it.positive-sign-exponent, decimal-separator: it.decimal-separator))

if it.math {
let power = math.attach([#it.base], t: [#exponent])
if it.product == none { (power,) }
Expand Down
76 changes: 57 additions & 19 deletions src/num.typ
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#import "parsing.typ" as parsing: nonum

#let update-state(state, args, name: none) = {
assert-no-fixed(args)
state.update(s => {
assert-settable-args(args, s, name: name)
s + args.named()
Expand Down Expand Up @@ -44,6 +45,55 @@
}


#let process-exponent(info, exponent) = {
let new-exponent = if type(exponent) == dictionary {
assert(
"fixed" in exponent or "sci" in exponent,
message: "Expected key \"fixed\" or \"sci\", got " + repr(exponent)
)

if "fixed" in exponent {
exponent.fixed
} else {
let threshold = exponent.sci
if type(threshold) == int {
threshold = (-threshold, threshold)
}
let e = parsing.compute-sci-digits(info)
if e > threshold.at(0) and e < threshold.at(1) {
return info
}
e
}
} else if exponent == "eng" {
parsing.compute-eng-digits(info)
} else if exponent == "sci" {
parsing.compute-sci-digits(info)
}

let e = if info.e == none { 0 } else { int(info.e) }
// let significant-figures = (info.int + info.frac).trim("0").len()

let shift = utility.shift-decimal-left.with(digits: new-exponent - e)

info.e = str(new-exponent).replace("−", "-")
(info.int, info.frac) = shift(info.int, info.frac)

if info.pm != none {
if type(info.pm.first()) != array {
info.pm = shift(..info.pm)
} else {
info.pm = pm.map(x => shift(..x))
}
}
// if info.int != "0" {
// info.frac = info.frac.slice(0, calc.max(0, significant-figures - info.int.len()))
// }

info
}



#let show-num = it => {

Expand All @@ -59,28 +109,14 @@
}
if "sign" not in info {info.sign = "" }
} else {
let num-str = number-to-string(it.number)
if num-str == none {
assert(false, message: "Cannot parse the number `" + repr(it.number) + "`")
}
info = decompose-normalized-number-string(num-str)
info = parse-numeral(it.number)
}

/// Maybe shift exponent
if it.fixed != none {
let e = if info.e == none { 0 } else { int(info.e) }
let shift(int, frac) = utility.shift-decimal-left(int, frac, it.fixed - e)
(info.int, info.frac) = shift(info.int, info.frac)

if info.pm != none {
if type(info.pm.first()) != array {
info.pm = shift(..info.pm)
} else {
info.pm = pm.map(x => shift(..x))
}
if it.exponent != auto {
info = process-exponent(info, it.exponent)
if "prefixed-eng" in it {
info.e = none
}

info.e = str(it.fixed).replace("−", "-")
}

/// Round number and uncertainty
Expand Down Expand Up @@ -146,6 +182,8 @@
force-parentheses-around-uncertainty: false,
..args
) = {
assert-no-fixed(args)

let inline-args = (
align: align,
prefix: prefix,
Expand Down
41 changes: 36 additions & 5 deletions src/parsing.typ
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
}

if normalize-pm {
pm = utility.shift-decimal-left(..pm, fractional.len())
pm = utility.shift-decimal-left(..pm, digits: fractional.len())
}
}
if integer == "" { integer = "0" }
Expand All @@ -226,15 +226,15 @@
)
#assert.eq(
decompose-normalized-number-string(".4(2)"),
(sign: "+", int: "0", frac: "4", pm: ("", "2"), e: none)
(sign: "+", int: "0", frac: "4", pm: ("0", "2"), e: none)
)
#assert.eq(
decompose-normalized-number-string(".4333(2)"),
(sign: "+", int: "0", frac: "4333", pm: ("", "0002"), e: none)
(sign: "+", int: "0", frac: "4333", pm: ("0", "0002"), e: none)
)
#assert.eq(
decompose-normalized-number-string(".4333(200)"),
(sign: "+", int: "0", frac: "4333", pm: ("", "0200"), e: none)
(sign: "+", int: "0", frac: "4333", pm: ("0", "0200"), e: none)
)
#assert.eq(
decompose-normalized-number-string(".43(200)"),
Expand All @@ -246,5 +246,36 @@
)
#assert.eq(
decompose-normalized-number-string("2.3(2.9)"),
(sign: "+", int: "2", frac: "3", pm: ("", "29"), e: none)
(sign: "+", int: "2", frac: "3", pm: ("0", "29"), e: none)
)


#let parse-numeral(input) = {

let num-str = number-to-string(input)
if num-str == none {
assert(false, message: "Cannot parse the number `" + repr(it.number) + "`")
}
decompose-normalized-number-string(num-str)
}


#let compute-sci-digits(num-info) = {
let integer = num-info.int
let fractional = num-info.frac
let e = if num-info.e == none { 0 } else { int(num-info.e) }

let exponent = 0
if integer == "0" {
let leading-zeros = fractional.len() - fractional.trim("0", at: start).len()

exponent = -leading-zeros - 1
} else {
exponent = integer.len() - 1
}
exponent + e
}

#let compute-eng-digits(num-info) = {
calc.floor(compute-sci-digits(num-info) / 3) * 3
}
18 changes: 12 additions & 6 deletions src/state.typ
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
#let default-state = (
// Mantissa and uncertainty
digits: auto,
fixed: none,
product: sym.times,
decimal-separator: ".",
tight: false,
omit-unity-mantissa: false,
uncertainty-mode: "separate",
positive-sign: false,
tight: false,
math: true,

// Power
product: sym.times,
positive-sign-exponent: false,
base: 10,
uncertainty-mode: "separate",
math: true,
fixed: none,
exponent: auto,

group: (
size: 3,
separator: sym.space.thin,
Expand All @@ -26,7 +31,8 @@
unit-separator: sym.space.thin,
fraction: "power",
breakable: false,
use-sqrt: true
use-sqrt: true,
prefix: auto
)
)
#let num-state = state("num-state", default-state)
Expand Down
37 changes: 35 additions & 2 deletions src/units.typ
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#import "num.typ": num
#import "state.typ": num-state, update-num-state
#import "assertations.typ": assert-settable-args

#import "parsing.typ": parse-numeral, compute-eng-digits
#import "utility.typ"

/// [internal function]
/// Parse a text-based unit specification.
Expand Down Expand Up @@ -243,7 +244,8 @@
unit,
..args
) = context {

let unit = unit

let num-state = update-num-state(num-state.get(), (unit: args.named()) + args.named())

let separator = sym.space.thin
Expand All @@ -254,6 +256,37 @@
separator = none
}

if num-state.unit.prefix == auto and num-state.exponent == "eng" {

num-state.prefixed-eng = true

let info = parse-numeral(value)
let e = if info.e == none { 0 } else { int(info.e) }
let eng = compute-eng-digits(info)

if eng != 0 {
let prefixes = (
"3": [k],
"6": [M],
"9": [G],
"12": [T],
"15": [P],
"18": [E],
"−3": [m],
"−6": [#sym.mu],
"−9": [n],
"−12": [p],
"−15": [f],
"−18": [a],
)


let prefix = prefixes.at(str(eng))
assert(unit.numerator.len() != 0)
unit.numerator.first().first() = prefix + unit.numerator.first().first()
}
}

let result = {
num(value, state: num-state, force-parentheses-around-uncertainty: true) // force parens around numbers with uncertainty
separator
Expand Down
19 changes: 11 additions & 8 deletions src/utility.typ
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// for `digits` produce a right-shift. Numbers are automatically
/// padded with zeros but both integer and fractional parts
/// may become "empty" when they are zero.
#let shift-decimal-left(integer, fractional, digits) = {
#let shift-decimal-left(integer, fractional, digits: 0) = {
if digits < 0 {
let available-digits = calc.min(-digits, fractional.len())
integer += fractional.slice(0, available-digits)
Expand All @@ -19,13 +19,16 @@
fractional = "0" * (digits - available-digits) + fractional
integer = integer.slice(0, integer.len() - available-digits)
}
if integer == "" {
integer = "0"
}
return (integer, fractional)
}

#assert.eq(shift-decimal-left("123", "456", 0), ("123", "456"))
#assert.eq(shift-decimal-left("123", "456", 2), ("1", "23456"))
#assert.eq(shift-decimal-left("123", "456", 5), ("", "00123456"))
#assert.eq(shift-decimal-left("123", "456", -2), ("12345", "6"))
#assert.eq(shift-decimal-left("123", "456", -5), ("12345600", ""))
#assert.eq(shift-decimal-left("0", "0012", -4), ("12", ""))
#assert.eq(shift-decimal-left("0", "0012", -2), ("", "12"))
#assert.eq(shift-decimal-left("123", "456", digits: 0), ("123", "456"))
#assert.eq(shift-decimal-left("123", "456", digits: 2), ("1", "23456"))
#assert.eq(shift-decimal-left("123", "456", digits: 5), ("0", "00123456"))
#assert.eq(shift-decimal-left("123", "456", digits: -2), ("12345", "6"))
#assert.eq(shift-decimal-left("123", "456", digits: -5), ("12345600", ""))
#assert.eq(shift-decimal-left("0", "0012", digits: -4), ("12", ""))
#assert.eq(shift-decimal-left("0", "0012", digits: -2), ("0", "12"))
Loading