Skip to content

Commit afa1e63

Browse files
authored
[add] exponent modes (#45)
1 parent 1140b29 commit afa1e63

File tree

34 files changed

+347
-66
lines changed

34 files changed

+347
-66
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Zero's core is the `num()` function, which provides flexible number formatting.
9999
#num(
100100
number: str | content | int | float | dictionary | array,
101101
digits: auto | int = auto,
102-
fixed: none | int = none,
102+
exponent: auto | str = auto,
103103
104104
decimal-separator: str = ".",
105105
product: content = sym.times,
@@ -116,7 +116,12 @@ Zero's core is the `num()` function, which provides flexible number formatting.
116116
```
117117
- `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).
118118
- `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).
119-
- `fixed: none | int = none` : If not `none`, forces a fixed exponent. Additional exponents given in the number input are taken into account.
119+
- `exponent: auto | str = auto` : Controls the value of the exponent. Possible values are
120+
- `auto` : The exponent is taken to be the _input exponent_, e.g., `5` for `num[1e5]`.
121+
- `"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`.
122+
- `"eng"` : The exponent chosen according to engineering notation, that is the exponent is a multiple of three and the integer part is never zero.
123+
- `(fixed: n)` : The exponent is fixed to the given integer.
124+
120125
- `decimal-separator: str = "."` : Specifies the marker that is used for separating integer and decimal part.
121126
- `product: content = sym.times` : Specifies the multiplication symbol used for scientific notation.
122127
- `tight: bool = false` : If true, tight spacing is applied between operands (applies to $\times$ and $\pm$).
@@ -399,7 +404,8 @@ The appearance of units can be configured via `set-unit`:
399404
#set-unit(
400405
unit-separator: content = sym.space.thin,
401406
fraction: str = "power",
402-
breakable: bool = false
407+
breakable: bool = false,
408+
prefix auto | none = auto
403409
)
404410
```
405411
- `unit-separator: content` : Configures the separator between consecutive unit parts in a composite unit.
@@ -408,6 +414,7 @@ The appearance of units can be configured via `set-unit`:
408414
- `"fraction"` : When units with negative exponents are present, a fraction is created and the concerned units are put in the denominator.
409415
- `"inline"` : An inline fraction is created.
410416
- `breakable: bool` : Whether units and quantities can be broken across paragraph lines.
417+
- `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.
411418

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

src/assertations.typ

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,15 @@
4444
assert(false, message: message)
4545
}
4646
}
47+
48+
49+
#let assert-no-fixed(args) = {
50+
return
51+
if "fixed" in args.named() {
52+
let value = str(args.at("fixed"))
53+
assert(
54+
false,
55+
message: "The parameter `fixed` has been removed. Instead use `exponent (fixed: " + value + "`) instead of `fixed: " + value + "`"
56+
)
57+
}
58+
}

src/formatting.typ

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,11 @@
1919
/// if `positive-sign` is set to true. In all other cases, the result is
2020
/// `none`.
2121
#let format-sign(sign, positive-sign: false) = {
22-
if sign == "-" { return "" }
23-
else if sign == "+" and positive-sign { return "+" }
22+
if sign == "-" { return math.class("unary", sym.minus) }
23+
else if sign == "+" and positive-sign { return math.class("unary", sym.plus) }
2424
}
2525

26-
#assert.eq(format-sign("-", positive-sign: false), "")
27-
#assert.eq(format-sign("+", positive-sign: false), none)
28-
#assert.eq(format-sign("-", positive-sign: true), "")
29-
#assert.eq(format-sign("+", positive-sign: true), "+")
30-
#assert.eq(format-sign(none, positive-sign: true), none)
26+
3127

3228

3329

@@ -163,7 +159,7 @@
163159
)
164160

165161
if compact-pm {
166-
pm = pm.map(x => utility.shift-decimal-left(..x, -it.digits))
162+
pm = pm.map(x => utility.shift-decimal-left(..x, digits: -it.digits))
167163
it.digits = auto
168164
}
169165
}
@@ -215,7 +211,7 @@
215211

216212
let (sign, integer, fractional) = decompose-signed-float-string(it.exponent)
217213
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))
218-
214+
219215
if it.math {
220216
let power = math.attach([#it.base], t: [#exponent])
221217
if it.product == none { (power,) }

src/num.typ

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#import "parsing.typ" as parsing: nonum
66

77
#let update-state(state, args, name: none) = {
8+
assert-no-fixed(args)
89
state.update(s => {
910
assert-settable-args(args, s, name: name)
1011
s + args.named()
@@ -44,6 +45,55 @@
4445
}
4546

4647

48+
#let process-exponent(info, exponent) = {
49+
let new-exponent = if type(exponent) == dictionary {
50+
assert(
51+
"fixed" in exponent or "sci" in exponent,
52+
message: "Expected key \"fixed\" or \"sci\", got " + repr(exponent)
53+
)
54+
55+
if "fixed" in exponent {
56+
exponent.fixed
57+
} else {
58+
let threshold = exponent.sci
59+
if type(threshold) == int {
60+
threshold = (-threshold, threshold)
61+
}
62+
let e = parsing.compute-sci-digits(info)
63+
if e > threshold.at(0) and e < threshold.at(1) {
64+
return info
65+
}
66+
e
67+
}
68+
} else if exponent == "eng" {
69+
parsing.compute-eng-digits(info)
70+
} else if exponent == "sci" {
71+
parsing.compute-sci-digits(info)
72+
}
73+
74+
let e = if info.e == none { 0 } else { int(info.e) }
75+
// let significant-figures = (info.int + info.frac).trim("0").len()
76+
77+
let shift = utility.shift-decimal-left.with(digits: new-exponent - e)
78+
79+
info.e = str(new-exponent).replace("", "-")
80+
(info.int, info.frac) = shift(info.int, info.frac)
81+
82+
if info.pm != none {
83+
if type(info.pm.first()) != array {
84+
info.pm = shift(..info.pm)
85+
} else {
86+
info.pm = pm.map(x => shift(..x))
87+
}
88+
}
89+
// if info.int != "0" {
90+
// info.frac = info.frac.slice(0, calc.max(0, significant-figures - info.int.len()))
91+
// }
92+
93+
info
94+
}
95+
96+
4797

4898
#let show-num = it => {
4999

@@ -59,28 +109,14 @@
59109
}
60110
if "sign" not in info {info.sign = "" }
61111
} else {
62-
let num-str = number-to-string(it.number)
63-
if num-str == none {
64-
assert(false, message: "Cannot parse the number `" + repr(it.number) + "`")
65-
}
66-
info = decompose-normalized-number-string(num-str)
112+
info = parse-numeral(it.number)
67113
}
68114

69-
/// Maybe shift exponent
70-
if it.fixed != none {
71-
let e = if info.e == none { 0 } else { int(info.e) }
72-
let shift(int, frac) = utility.shift-decimal-left(int, frac, it.fixed - e)
73-
(info.int, info.frac) = shift(info.int, info.frac)
74-
75-
if info.pm != none {
76-
if type(info.pm.first()) != array {
77-
info.pm = shift(..info.pm)
78-
} else {
79-
info.pm = pm.map(x => shift(..x))
80-
}
115+
if it.exponent != auto {
116+
info = process-exponent(info, it.exponent)
117+
if "prefixed-eng" in it {
118+
info.e = none
81119
}
82-
83-
info.e = str(it.fixed).replace("", "-")
84120
}
85121

86122
/// Round number and uncertainty
@@ -146,6 +182,8 @@
146182
force-parentheses-around-uncertainty: false,
147183
..args
148184
) = {
185+
assert-no-fixed(args)
186+
149187
let inline-args = (
150188
align: align,
151189
prefix: prefix,

src/parsing.typ

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204
}
205205

206206
if normalize-pm {
207-
pm = utility.shift-decimal-left(..pm, fractional.len())
207+
pm = utility.shift-decimal-left(..pm, digits: fractional.len())
208208
}
209209
}
210210
if integer == "" { integer = "0" }
@@ -226,15 +226,15 @@
226226
)
227227
#assert.eq(
228228
decompose-normalized-number-string(".4(2)"),
229-
(sign: "+", int: "0", frac: "4", pm: ("", "2"), e: none)
229+
(sign: "+", int: "0", frac: "4", pm: ("0", "2"), e: none)
230230
)
231231
#assert.eq(
232232
decompose-normalized-number-string(".4333(2)"),
233-
(sign: "+", int: "0", frac: "4333", pm: ("", "0002"), e: none)
233+
(sign: "+", int: "0", frac: "4333", pm: ("0", "0002"), e: none)
234234
)
235235
#assert.eq(
236236
decompose-normalized-number-string(".4333(200)"),
237-
(sign: "+", int: "0", frac: "4333", pm: ("", "0200"), e: none)
237+
(sign: "+", int: "0", frac: "4333", pm: ("0", "0200"), e: none)
238238
)
239239
#assert.eq(
240240
decompose-normalized-number-string(".43(200)"),
@@ -246,5 +246,36 @@
246246
)
247247
#assert.eq(
248248
decompose-normalized-number-string("2.3(2.9)"),
249-
(sign: "+", int: "2", frac: "3", pm: ("", "29"), e: none)
249+
(sign: "+", int: "2", frac: "3", pm: ("0", "29"), e: none)
250250
)
251+
252+
253+
#let parse-numeral(input) = {
254+
255+
let num-str = number-to-string(input)
256+
if num-str == none {
257+
assert(false, message: "Cannot parse the number `" + repr(it.number) + "`")
258+
}
259+
decompose-normalized-number-string(num-str)
260+
}
261+
262+
263+
#let compute-sci-digits(num-info) = {
264+
let integer = num-info.int
265+
let fractional = num-info.frac
266+
let e = if num-info.e == none { 0 } else { int(num-info.e) }
267+
268+
let exponent = 0
269+
if integer == "0" {
270+
let leading-zeros = fractional.len() - fractional.trim("0", at: start).len()
271+
272+
exponent = -leading-zeros - 1
273+
} else {
274+
exponent = integer.len() - 1
275+
}
276+
exponent + e
277+
}
278+
279+
#let compute-eng-digits(num-info) = {
280+
calc.floor(compute-sci-digits(num-info) / 3) * 3
281+
}

src/state.typ

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
#let default-state = (
2+
// Mantissa and uncertainty
23
digits: auto,
3-
fixed: none,
4-
product: sym.times,
54
decimal-separator: ".",
6-
tight: false,
75
omit-unity-mantissa: false,
6+
uncertainty-mode: "separate",
87
positive-sign: false,
8+
tight: false,
9+
math: true,
10+
11+
// Power
12+
product: sym.times,
913
positive-sign-exponent: false,
1014
base: 10,
11-
uncertainty-mode: "separate",
12-
math: true,
15+
fixed: none,
16+
exponent: auto,
17+
1318
group: (
1419
size: 3,
1520
separator: sym.space.thin,
@@ -26,7 +31,8 @@
2631
unit-separator: sym.space.thin,
2732
fraction: "power",
2833
breakable: false,
29-
use-sqrt: true
34+
use-sqrt: true,
35+
prefix: auto
3036
)
3137
)
3238
#let num-state = state("num-state", default-state)

src/units.typ

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#import "num.typ": num
22
#import "state.typ": num-state, update-num-state
33
#import "assertations.typ": assert-settable-args
4-
4+
#import "parsing.typ": parse-numeral, compute-eng-digits
5+
#import "utility.typ"
56

67
/// [internal function]
78
/// Parse a text-based unit specification.
@@ -243,7 +244,8 @@
243244
unit,
244245
..args
245246
) = context {
246-
247+
let unit = unit
248+
247249
let num-state = update-num-state(num-state.get(), (unit: args.named()) + args.named())
248250

249251
let separator = sym.space.thin
@@ -254,6 +256,37 @@
254256
separator = none
255257
}
256258

259+
if num-state.unit.prefix == auto and num-state.exponent == "eng" {
260+
261+
num-state.prefixed-eng = true
262+
263+
let info = parse-numeral(value)
264+
let e = if info.e == none { 0 } else { int(info.e) }
265+
let eng = compute-eng-digits(info)
266+
267+
if eng != 0 {
268+
let prefixes = (
269+
"3": [k],
270+
"6": [M],
271+
"9": [G],
272+
"12": [T],
273+
"15": [P],
274+
"18": [E],
275+
"−3": [m],
276+
"−6": [#sym.mu],
277+
"−9": [n],
278+
"−12": [p],
279+
"−15": [f],
280+
"−18": [a],
281+
)
282+
283+
284+
let prefix = prefixes.at(str(eng))
285+
assert(unit.numerator.len() != 0)
286+
unit.numerator.first().first() = prefix + unit.numerator.first().first()
287+
}
288+
}
289+
257290
let result = {
258291
num(value, state: num-state, force-parentheses-around-uncertainty: true) // force parens around numbers with uncertainty
259292
separator

src/utility.typ

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
/// for `digits` produce a right-shift. Numbers are automatically
55
/// padded with zeros but both integer and fractional parts
66
/// may become "empty" when they are zero.
7-
#let shift-decimal-left(integer, fractional, digits) = {
7+
#let shift-decimal-left(integer, fractional, digits: 0) = {
88
if digits < 0 {
99
let available-digits = calc.min(-digits, fractional.len())
1010
integer += fractional.slice(0, available-digits)
@@ -19,13 +19,16 @@
1919
fractional = "0" * (digits - available-digits) + fractional
2020
integer = integer.slice(0, integer.len() - available-digits)
2121
}
22+
if integer == "" {
23+
integer = "0"
24+
}
2225
return (integer, fractional)
2326
}
2427

25-
#assert.eq(shift-decimal-left("123", "456", 0), ("123", "456"))
26-
#assert.eq(shift-decimal-left("123", "456", 2), ("1", "23456"))
27-
#assert.eq(shift-decimal-left("123", "456", 5), ("", "00123456"))
28-
#assert.eq(shift-decimal-left("123", "456", -2), ("12345", "6"))
29-
#assert.eq(shift-decimal-left("123", "456", -5), ("12345600", ""))
30-
#assert.eq(shift-decimal-left("0", "0012", -4), ("12", ""))
31-
#assert.eq(shift-decimal-left("0", "0012", -2), ("", "12"))
28+
#assert.eq(shift-decimal-left("123", "456", digits: 0), ("123", "456"))
29+
#assert.eq(shift-decimal-left("123", "456", digits: 2), ("1", "23456"))
30+
#assert.eq(shift-decimal-left("123", "456", digits: 5), ("0", "00123456"))
31+
#assert.eq(shift-decimal-left("123", "456", digits: -2), ("12345", "6"))
32+
#assert.eq(shift-decimal-left("123", "456", digits: -5), ("12345600", ""))
33+
#assert.eq(shift-decimal-left("0", "0012", digits: -4), ("12", ""))
34+
#assert.eq(shift-decimal-left("0", "0012", digits: -2), ("0", "12"))

0 commit comments

Comments
 (0)