├── .gitignore ├── LICENSE ├── README.md ├── examples ├── example.pdf ├── example.typ └── overview.jpg ├── format.typ ├── init.typ ├── lib.typ ├── money.csv ├── postfixes.csv ├── prefixes-en.csv ├── prefixes-ru.csv ├── typst.toml ├── units-en.csv └── units-ru.csv /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christopher Hecker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unify 2 | `unify` is a [Typst](https://github.com/typst/typst) package simplifying the typesetting of numbers, units, and ranges. It is the equivalent to LaTeX's `siunitx`, though not as mature. 3 | 4 | 5 | ## Overview 6 | `unify` allows flexible numbers and units, and still mostly gets well typeset results. 7 | ```typ 8 | #import "@preview/unify:0.7.1": num,qty,numrange,qtyrange 9 | 10 | $ num("-1.32865+-0.50273e-6") $ 11 | $ qty("1.3+1.2-0.3e3", "erg/cm^2/s", space: "#h(2mm)") $ 12 | $ numrange("1,1238e-2", "3,0868e5", thousandsep: "'") $ 13 | $ qtyrange("1e3", "2e3", "meter per second squared", per: "/", delimiter: "\"to\"") $ 14 | ``` 15 | 16 | 17 | Right now, physical, monetary, and binary units are supported. New issues or pull requests for new units are welcome! 18 | 19 | ## Multilingual support 20 | The Unify package supports multiple languages. Currently, the supported languages are English and Russian. The fallback is English. If you want to add your language, you should add two files: `prefixes-xx.csv` and `units-xx.csv`, and in the `lib.typ` file you should fix the `lang-db` state for your files. 21 | 22 | ## `num` 23 | `num` uses string parsing in order to typeset numbers, including separators between the thousands. They can have the following form: 24 | - `float` or `integer` number 25 | - either (`{}` stands for a number) 26 | - symmetric uncertainties with `+-{}` 27 | - asymmetric uncertainties with `+{}-{}` 28 | - exponential notation `e{}` 29 | 30 | Parentheses are automatically set as necessary. Use `thousandsep` to change the separator between the thousands, and `multiplier` to change the multiplication symbol between the number and exponential. 31 | 32 | *Note: Because units are evaluated in math mode, plain spaces (`thousandsep: " "`) will not have the desired effect. Use `thousandsep: "space"` instead.* 33 | 34 | 35 | ## `unit` 36 | `unit` takes the unit in words or in symbolic notation as its first argument. The value of `space` will be inserted between units if necessary. Setting `per` to `symbol` will format the number with exponents (i.e. `^(-1)`), `fraction` or `/` using fraction, and `fraction-short` or `\\/` using in-line fractions. 37 | Units in words have four possible parts: 38 | - `per` forms the inverse of the following unit. 39 | - A written-out prefix in the sense of SI (e.g. `centi`). This is added before the unit. 40 | - The unit itself written out (e.g. `gram`). 41 | - A postfix like `squared`. This is added after the unit and takes `per` into account. 42 | 43 | The shorthand notation also has four parts: 44 | - `/` forms the inverse of the following unit. 45 | - A short prefix in the sense of SI (e.g. `k`). This is added before the unit. 46 | - The short unit itself (e.g. `g`). 47 | - An exponent like `^2`. This is added after the unit and takes `/` into account. 48 | 49 | Note: Use `u` for micro. 50 | 51 | The possible values of the three latter parts are loaded at runtime from `prefixes.csv`, `units.csv`, and `postfixes.csv` (in the library directory). Your own units etc. can be permanently added in these files. At runtime, they can be added using `add-unit` and `add-prefix`, respectively. The formats for the pre- and postfixes are: 52 | 53 | | pre-/postfix | shorthand | symbol | 54 | | ------------ | --------- | ------------ | 55 | | milli | m | upright("m") | 56 | 57 | and for units: 58 | 59 | | unit | shorthand | symbol | space | 60 | | ----- | --------- | ------------ | ----- | 61 | | meter | m | upright("m") | true | 62 | 63 | The first column specifies the written-out word, the second one the shorthand. These should be unique. The third column represents the string that will be inserted as the unit symbol. For units, the last column describes whether there should be space before the unit (possible values: `true`/`false`, `1`,`0`). This is mostly the cases for degrees and other angle units (e.g. arcseconds). 64 | If you think there are units not included that are of interest for other users, you can create an issue or PR. 65 | 66 | 67 | ## `qty` 68 | `qty` allows a `num` as the first argument following the same rules. The second argument is a unit. If `rawunit` is set to true, its value will be passed on to the result (note that the string passed on will be passed to `eval`, so add escaped quotes `\"` if necessary). Otherwise, it follows the rules of `unit`. The value of `space` will be inserted between units if necessary, `thousandsep` between the thousands, and `per` switches between exponents and fractions. 69 | 70 | 71 | ## `numrange` 72 | `numrange` takes two `num`s as the first two arguments. If they have the same exponent, it is automatically factorized. The range symbol can be changed with `delimiter`, and the space between the numbers and symbols with `space`. 73 | 74 | 75 | ## `qtyrange` 76 | `qtyrange` is just a combination of `unit` and `range`. 77 | -------------------------------------------------------------------------------- /examples/example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChHecker/unify/b9f6d556a0514880d3b10e3dbf95b0eed6a7adad/examples/example.pdf -------------------------------------------------------------------------------- /examples/example.typ: -------------------------------------------------------------------------------- 1 | #import "../lib.typ": * 2 | 3 | #set text(lang: "en") 4 | Working with English characters: 5 | $ num("-1.32865+-0.50273e-6") $ 6 | $ qty("1.3+1.2-0.3e3", "erg/cm^2/s", space: "#h(2mm)") $ 7 | $ numrange("1,1238e-2", "3,0868e5", thousandsep: "'") $ 8 | $ qtyrange("1e3", "2e3", "meter per second squared", per: "/", delimiter: "\"to\"") $ 9 | $ qty("55.36", "usd") $ 10 | 11 | Adding your own prefix and unit: 12 | #add-prefix("pre", "P", "upright(\"pre\")") 13 | #add-unit("unit", "U", "bold(\"unit\")") 14 | $ unit("PU") $ 15 | 16 | #set text(lang: "ru") 17 | Работа пакета с русскими символами: 18 | $ num("-1.32865+-0.50273e-6") $ 19 | $ qty("1.3+1.2-0.3e3", "erg/cm^2/s", space: "#h(2mm)") $ 20 | $ numrange("1,1238e-2", "3,0868e5", thousandsep: "'") $ 21 | $ qtyrange("1e3", "2e3", "meter per second squared", per: "/", delimiter: "\"до\"") $ 22 | 23 | #set text(lang: "de") 24 | Other languages fall back to English units: 25 | $ num("-1.32865+-0.50273e-6") $ 26 | $ qty("1.3+1.2-0.3e3", "erg/cm^2/s", space: "#h(2mm)") $ 27 | $ numrange("1,1238e-2", "3,0868e5", thousandsep: "'") $ 28 | $ qtyrange("1e3", "2e3", "meter per second squared", per: "/", delimiter: "\"to\"") $ -------------------------------------------------------------------------------- /examples/overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChHecker/unify/b9f6d556a0514880d3b10e3dbf95b0eed6a7adad/examples/overview.jpg -------------------------------------------------------------------------------- /format.typ: -------------------------------------------------------------------------------- 1 | #import "init.typ": * 2 | 3 | #let _re-num = regex("^(-?\d+(\.|,)?\d*)?(((\+(\d+(\.|,)?\d*)-(\d+(\.|,)?\d*)))|((((\+-)|(-\+))(\d+(\.|,)?\d*))))?((e|E)([-\+]?\d+))?$") 4 | #let _unicode-exponents = ( 5 | ("\u2070", "0"), 6 | ("\u00B9", "1"), 7 | ("\u00B2", "2"), 8 | ("\u00B3", "3"), 9 | ("\u2074", "4"), 10 | ("\u2075", "5"), 11 | ("\u2076", "6"), 12 | ("\u2077", "7"), 13 | ("\u2078", "8"), 14 | ("\u2079", "9"), 15 | ("\u207A", "+"), 16 | ("\u207B", "-"), 17 | ) 18 | 19 | #let _format-float(f, decsep: "auto", thousandsep: "#h(0.166667em)") = { 20 | /// Formats a float with thousands separator. 21 | /// - `f`: Float to format. 22 | /// - `decsep`: Which decimal separator to use. This must be the same as the one used in `f`. Set it to `auto` to automatically choose it. Falls back to `.`. 23 | /// - `thousandsep`: The separator between the thousands. 24 | let string = "" 25 | if decsep == "auto" { 26 | if "," in f { 27 | decsep = "," 28 | } else { 29 | decsep = "." 30 | } 31 | } 32 | 33 | if thousandsep.trim() == "." { 34 | thousandsep = ".#h(0mm)" 35 | } 36 | 37 | let split = str(f).split(decsep) 38 | let int-part = split.at(0) 39 | let dec-part = split.at(1, default: none) 40 | let int-list = int-part.clusters() 41 | 42 | string += str(int-list.remove(0)) 43 | for (i, n) in int-list.enumerate() { 44 | let mod = (i - int-list.len()) / 3 45 | if int(mod) == mod { 46 | string += " " + thousandsep + " " 47 | } 48 | string += str(n) 49 | } 50 | 51 | if dec-part != none { 52 | let dec-list = dec-part.clusters() 53 | string += decsep 54 | for (i, n) in dec-list.enumerate() { 55 | let mod = i / 3 56 | if int(mod) == mod and i != 0 { 57 | string += " " + thousandsep + " " 58 | } 59 | string += str(n) 60 | } 61 | } 62 | 63 | string 64 | } 65 | 66 | #let _format-num( 67 | value, 68 | exponent: none, 69 | upper: none, 70 | lower: none, 71 | multiplier: "dot", 72 | thousandsep: "#h(0.166667em)", 73 | ) = { 74 | /// Format a number. 75 | /// - `value`: Value of the number. 76 | /// - `exponent`: Exponent in the exponential notation. 77 | /// - `upper`: Upper uncertainty. 78 | /// - `lower`: Lower uncertainty. 79 | /// - `multiplier`: The symbol used to indicate multiplication 80 | /// - `thousandsep`: The separator between the thousands of the float. 81 | 82 | let formatted-value = "" 83 | if value != none { 84 | formatted-value += _format-float(value, thousandsep: thousandsep).replace(",", ",#h(0pt)") 85 | } 86 | if upper != none and lower != none { 87 | if upper != lower { 88 | formatted-value += "^(+" + _format-float(upper, thousandsep: thousandsep) + ")" 89 | formatted-value += "_(-" + _format-float(lower, thousandsep: thousandsep) + ")" 90 | } else { 91 | formatted-value += " plus.minus " + _format-float(upper, thousandsep: thousandsep).replace(",", ",#h(0pt)") 92 | } 93 | } else if upper != none { 94 | formatted-value += " plus.minus " + _format-float(upper, thousandsep: thousandsep).replace(",", ",#h(0pt)") 95 | } else if lower != none { 96 | formatted-value += " plus.minus " + _format-float(lower, thousandsep: thousandsep).replace(",", ",#h(0pt)") 97 | } 98 | if not (upper == none and lower == none) { 99 | formatted-value = "lr((" + formatted-value 100 | formatted-value += "))" 101 | } 102 | if exponent != none { 103 | if value != none { 104 | formatted-value += " " + multiplier + " " 105 | } 106 | formatted-value += "10^(" + str(exponent) + ")" 107 | } 108 | formatted-value 109 | } 110 | 111 | #let _unicode-exponent-list = for (unicode, ascii) in _unicode-exponents { 112 | (unicode,) 113 | } 114 | 115 | #let _exponent-pattern = regex("[" + _unicode-exponent-list.join("|") + "]+") 116 | 117 | #let _replace-unicode-exponents(unit-str) = { 118 | let exponent-matches = unit-str.matches(_exponent-pattern) 119 | let exponent = "" 120 | for match in exponent-matches { 121 | 122 | exponent = "^" + match.text 123 | for (unicode, ascii) in _unicode-exponents { 124 | exponent = exponent.replace(regex(unicode), ascii) 125 | } 126 | unit-str = unit-str.replace(match.text, exponent) 127 | } 128 | unit-str 129 | } 130 | 131 | #let _chunk(string, cond) = (string: string, cond: cond) 132 | 133 | #let _format-unit-short( 134 | string, 135 | space: "#h(0.166667em)", 136 | per: "symbol", 137 | units-short, 138 | units-short-space, 139 | prefixes-short, 140 | ) = { 141 | /// Format a unit using the shorthand notation. 142 | /// - `string`: String containing the unit. 143 | /// - `space`: Space between units. 144 | /// - `per`: Whether to format the units after `/` with a fraction or exponent. 145 | 146 | assert(("symbol", "fraction", "/", "fraction-short", "short-fraction", "\\/").contains(per)) 147 | 148 | let formatted = "" 149 | 150 | string = _replace-unicode-exponents(string) 151 | 152 | let split = string.replace(regex(" */ *"), "/").replace(regex(" +"), " ").split(regex(" ")) 153 | let chunks = () 154 | for s in split { 155 | let per-split = s.split("/") 156 | chunks.push(_chunk(per-split.at(0), false)) 157 | if per-split.len() > 1 { 158 | for p in per-split.slice(1) { 159 | chunks.push(_chunk(p, true)) 160 | } 161 | } 162 | } 163 | 164 | // needed for fraction formatting 165 | let normal-list = () 166 | let per-list = () 167 | 168 | let prefixes = () 169 | for (string: string, cond: per-set) in chunks { 170 | let u-space = true 171 | let prefix = none 172 | let unit = "" 173 | let exponent = none 174 | 175 | let qty-exp = string.split("^") 176 | let quantity = qty-exp.at(0) 177 | exponent = qty-exp.at(1, default: none) 178 | 179 | if quantity in units-short { 180 | // Match units without prefixes 181 | unit = units-short.at(quantity) 182 | u-space = units-short-space.at(quantity) 183 | } else { 184 | // Match prefix + unit 185 | let pre = "" 186 | for char in quantity.clusters() { 187 | pre += char 188 | // Divide `quantity` into `pre`+`u` and check validity 189 | if pre in prefixes-short { 190 | let u = quantity.trim(pre, at: start, repeat: false) 191 | if u in units-short { 192 | prefix = prefixes-short.at(pre) 193 | unit = units-short.at(u) 194 | u-space = units-short-space.at(u) 195 | 196 | pre = none 197 | break 198 | } 199 | } 200 | } 201 | // if pre != none { 202 | // panic("invalid unit: " + quantity) 203 | // } 204 | } 205 | 206 | if per == "symbol" { 207 | if u-space { 208 | formatted += space 209 | } 210 | formatted += prefix + unit 211 | if exponent != none { 212 | if per-set { 213 | formatted += "^(-" + exponent + ")" 214 | } else { 215 | formatted += "^(" + exponent + ")" 216 | } 217 | } else if per-set { 218 | formatted += "^(-1)" 219 | } 220 | } else { 221 | let final-unit = "" 222 | // if u-space { 223 | // final-unit += space 224 | // } 225 | final-unit += prefix + unit 226 | if exponent != none { 227 | final-unit += "^(" + exponent + ")" 228 | } 229 | 230 | if per-set { 231 | per-list.push(_chunk(final-unit, u-space)) 232 | } else { 233 | normal-list.push(_chunk(final-unit, u-space)) 234 | } 235 | } 236 | } 237 | 238 | if per == "fraction" or per == "/" { 239 | if normal-list.at(0).at("cond") { 240 | formatted += space 241 | } 242 | 243 | if per-list.len() > 0 { 244 | formatted += " (" 245 | } 246 | 247 | for (i, chunk) in normal-list.enumerate() { 248 | let (string: n, cond: space-set) = chunk 249 | if i != 0 and space-set { 250 | formatted += space 251 | } 252 | formatted += n 253 | } 254 | 255 | if per-list.len() == 0 { 256 | return formatted 257 | } 258 | 259 | formatted += ")/(" 260 | for (i, chunk) in per-list.enumerate() { 261 | let (string: p, cond: space-set) = chunk 262 | if i != 0 and space-set { 263 | formatted += space 264 | } 265 | formatted += p 266 | } 267 | formatted += ")" 268 | } else if per == "fraction-short" or per == "\\/" { 269 | if normal-list.at(0).at("cond") { 270 | formatted += space 271 | } 272 | 273 | for (i, chunk) in normal-list.enumerate() { 274 | let (string: n, cond: space-set) = chunk 275 | formatted += n 276 | } 277 | 278 | for (i, chunk) in per-list.enumerate() { 279 | let (string: p, cond: space-set) = chunk 280 | formatted += "\\/" + p 281 | } 282 | } 283 | 284 | formatted 285 | } 286 | 287 | #let _format-unit(string, space: "#h(0.166667em)", per: "symbol") = { 288 | /// Format a unit using written-out words. 289 | /// - `string`: String containing the unit. 290 | /// - `space`: Space between units. 291 | /// - `per`: Whether to format the units after `per` with a fraction or exponent. 292 | 293 | assert(("symbol", "fraction", "/", "fraction-short", "short-fraction", "\\/").contains(per)) 294 | 295 | // load data 296 | let (units, units-short, units-space, units-short-space) = _units() 297 | 298 | let (prefixes, prefixes-short) = _prefixes() 299 | 300 | let formatted = "" 301 | 302 | // needed for fraction formatting 303 | let normal-list = () 304 | let per-list = () 305 | 306 | // whether per was used 307 | let per-set = false 308 | // whether waiting for a postfix 309 | let post = false 310 | // one unit 311 | let unit = _chunk("", true) 312 | 313 | let split = lower(string).split(" ") 314 | split.push("") 315 | 316 | for u in split { 317 | // expecting postfix 318 | if post { 319 | if per == "symbol" { 320 | // add postfix 321 | if u in _postfixes { 322 | if per-set { 323 | unit.at("string") += "^(-" 324 | } else { 325 | unit.at("string") += "^(" 326 | } 327 | unit.at("string") += _postfixes.at(u) 328 | unit.at("string") += ")" 329 | 330 | if unit.at("cond") { 331 | unit.at("string") = space + unit.at("string") 332 | } 333 | 334 | per-set = false 335 | post = false 336 | 337 | formatted += unit.at("string") 338 | unit = _chunk("", true) 339 | continue 340 | // add per 341 | } else if per-set { 342 | unit.at("string") += "^(-1)" 343 | 344 | if unit.at("cond") { 345 | unit.at("string") = space + unit.at("string") 346 | } 347 | 348 | per-set = false 349 | post = false 350 | 351 | formatted += unit.at("string") 352 | unit = _chunk("", true) 353 | // finish unit 354 | } else { 355 | post = false 356 | 357 | if unit.at("cond") { 358 | unit.at("string") = space + unit.at("string") 359 | } 360 | 361 | formatted += unit.at("string") 362 | unit = _chunk("", true) 363 | } 364 | } else { 365 | if u in _postfixes { 366 | unit.at("string") += "^(" 367 | unit.at("string") += _postfixes.at(u) 368 | unit.at("string") += ")" 369 | 370 | if per-set { 371 | per-list.push(unit) 372 | } else { 373 | normal-list.push(unit) 374 | } 375 | 376 | per-set = false 377 | post = false 378 | 379 | unit = _chunk("", true) 380 | continue 381 | } else { 382 | if per-set { 383 | per-list.push(unit) 384 | } else { 385 | normal-list.push(unit) 386 | } 387 | 388 | per-set = false 389 | post = false 390 | 391 | unit = _chunk("", true) 392 | } 393 | } 394 | } 395 | 396 | // detected per 397 | if u == "per" { 398 | per-set = true 399 | // add prefix 400 | } else if u in prefixes { 401 | unit.at("string") += prefixes.at(u) 402 | // add unit 403 | } else if u in units { 404 | unit.at("string") += units.at(u) 405 | unit.at("cond") = units-space.at(u) 406 | post = true 407 | } else if u != "" { 408 | return _format-unit-short(string, space: space, per: per, units-short, units-short-space, prefixes-short) 409 | } 410 | } 411 | 412 | if per == "fraction" or per == "/" { 413 | if normal-list.at(0).at("cond") { 414 | formatted += space 415 | } 416 | 417 | if per-list.len() > 0 { 418 | formatted += " (" 419 | } 420 | 421 | for (i, chunk) in normal-list.enumerate() { 422 | let (string: n, cond: space-set) = chunk 423 | if i != 0 and space-set { 424 | formatted += space 425 | } 426 | formatted += n 427 | } 428 | 429 | if per-list.len() == 0 { 430 | return formatted 431 | } 432 | 433 | formatted += ")/(" 434 | for (i, chunk) in per-list.enumerate() { 435 | let (string: p, cond: space-set) = chunk 436 | if i != 0 and space-set { 437 | formatted += space 438 | } 439 | formatted += p 440 | } 441 | formatted += ")" 442 | } else if per == "fraction-short" or per == "\\/" { 443 | if normal-list.at(0).at("cond") { 444 | formatted += space 445 | } 446 | 447 | for (i, chunk) in normal-list.enumerate() { 448 | let (string: n, cond: space-set) = chunk 449 | formatted += n 450 | } 451 | 452 | for (i, chunk) in per-list.enumerate() { 453 | let (string: p, cond: space-set) = chunk 454 | formatted += "\\/" + p 455 | } 456 | } 457 | 458 | formatted 459 | } 460 | 461 | #let _format-range( 462 | lower, 463 | upper, 464 | exponent-lower: none, 465 | exponent-upper: none, 466 | multiplier: "dot", 467 | delimiter: "-", 468 | space: "#h(0.16667em)", 469 | thousandsep: "#h(0.166667em)", 470 | force-parentheses: false, 471 | ) = { 472 | /// Format a range. 473 | /// - `(lower, upper)`: Strings containing the numbers. 474 | /// - `(exponent-lower, exponent-upper)`: Strings containing the exponentials in exponential notation. 475 | /// - `multiplier`: The symbol used to indicate multiplication 476 | /// - `delimiter`: Symbol between the numbers. 477 | /// - `space`: Space between the numbers and the delimiter. 478 | /// - `thousandsep`: The separator between the thousands of the float. 479 | /// - `force-parentheses`: Whether to force parentheses around the range. 480 | 481 | let formatted-value = "" 482 | 483 | formatted-value += _format-num(lower, thousandsep: thousandsep).replace(",", ",#h(0pt)") 484 | if exponent-lower != exponent-upper and exponent-lower != none { 485 | if lower != none { 486 | formatted-value += multiplier + " " 487 | } 488 | formatted-value += "10^(" + str(exponent-lower) + ")" 489 | } 490 | formatted-value += space + " " + delimiter + " " + space + _format-num(upper, thousandsep: thousandsep).replace( 491 | ",", 492 | ",#h(0pt)", 493 | ) 494 | if exponent-lower != exponent-upper and exponent-upper != none { 495 | if upper != none { 496 | formatted-value += multiplier + " " 497 | } 498 | formatted-value += "10^(" + str(exponent-upper) + ")" 499 | } 500 | if exponent-lower == exponent-upper and (exponent-lower != none and exponent-upper != none) { 501 | formatted-value = "lr((" + formatted-value 502 | formatted-value += ")) " + multiplier + " 10^(" + str(exponent-lower) + ")" 503 | } else if force-parentheses { 504 | formatted-value = "lr((" + formatted-value 505 | formatted-value += "))" 506 | } 507 | formatted-value 508 | } 509 | -------------------------------------------------------------------------------- /init.typ: -------------------------------------------------------------------------------- 1 | #let _prefix-csv(path, delimiter: ",") = { 2 | /// Load a CSV file with pre- or postfixes. 3 | /// - `path`: Path of the CSV file. 4 | /// - `delimiter`: Passed to the `csv` function. 5 | 6 | let array = csv(path, delimiter: delimiter) 7 | let symbols = (:) 8 | let symbols-short = (:) 9 | 10 | for line in array { 11 | symbols.insert(lower(line.at(0)), line.at(2)) 12 | symbols-short.insert(line.at(1), line.at(2)) 13 | } 14 | (symbols, symbols-short) 15 | } 16 | 17 | #let _postfix-csv(path, delimiter: ",") = { 18 | /// Load a CSV file with pre- or postfixes. 19 | /// - `path`: Path of the CSV file. 20 | /// - `delimiter`: Passed to the `csv` function. 21 | 22 | let array = csv(path, delimiter: delimiter) 23 | let dict = (:) 24 | 25 | for line in array { 26 | dict.insert(lower(line.at(0)), line.at(1)) 27 | } 28 | dict 29 | } 30 | 31 | #let _unit-csv(path, delimiter: ",") = { 32 | /// Load a CSV file with units. 33 | /// - `path`: Path of the CSV file. 34 | /// - `delimiter`: Passed to the `csv` function. 35 | 36 | let array = csv(path, delimiter: delimiter) 37 | let units = (:) 38 | let units-short = (:) 39 | let units-space = (:) 40 | let units-short-space = (:) 41 | 42 | for line in array { 43 | units.insert(lower(line.at(0)), line.at(2)) 44 | units-short.insert(line.at(1), line.at(2)) 45 | if line.at(3) == "false" or line.at(3) == "0" { 46 | units-space.insert(lower(line.at(0)), false) 47 | units-short-space.insert(line.at(1), false) 48 | } else { 49 | units-space.insert(lower(line.at(0)), true) 50 | units-short-space.insert(line.at(1), true) 51 | } 52 | } 53 | 54 | (units, units-short, units-space, units-short-space) 55 | } 56 | 57 | #let _postfixes = _postfix-csv("postfixes.csv") 58 | 59 | #let _add-money-units(data) = { 60 | let (units, units-short, units-space, units-short-space) = data 61 | 62 | let array = csv("money.csv", delimiter: ",") 63 | for line in array { 64 | units.insert(lower(line.at(0)), line.at(2)) 65 | units-short.insert(line.at(1), line.at(2)) 66 | if line.at(3) == "false" or line.at(3) == "0" { 67 | units-space.insert(lower(line.at(0)), false) 68 | units-short-space.insert(line.at(1), false) 69 | } else { 70 | units-space.insert(lower(line.at(0)), true) 71 | units-short-space.insert(line.at(1), true) 72 | } 73 | } 74 | 75 | (units, units-short, units-space, units-short-space) 76 | } 77 | 78 | #let _lang-db = state( 79 | "lang-db", 80 | ( 81 | "en": ( 82 | "units": (_add-money-units(_unit-csv("units-en.csv"))), 83 | "prefixes": (_prefix-csv("prefixes-en.csv")), 84 | ), 85 | "ru": ( 86 | "units": (_add-money-units(_unit-csv("units-ru.csv"))), 87 | "prefixes": (_prefix-csv("prefixes-ru.csv")), 88 | ), 89 | ), 90 | ) 91 | 92 | #let _get-language() = { 93 | let lang = text.lang 94 | let data = _lang-db.get() 95 | if lang in data { 96 | lang 97 | } else { 98 | "en" 99 | } 100 | } 101 | 102 | // get prefixes 103 | #let _prefixes() = { 104 | let lang = text.lang 105 | let data = _lang-db.get() 106 | if lang in data { 107 | data.at(lang).prefixes 108 | } else { 109 | data.en.prefixes 110 | } 111 | } 112 | 113 | // get units 114 | #let _units() = { 115 | let lang = text.lang 116 | let data = _lang-db.get() 117 | 118 | if lang in data { 119 | data.at(lang).units 120 | } else { 121 | data.en.units 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib.typ: -------------------------------------------------------------------------------- 1 | #import "format.typ": * 2 | 3 | #let num(value, multiplier: "dot", thousandsep: "#h(0.166667em)") = { 4 | /// Format a number. 5 | /// - `value`: String with the number. 6 | /// - `multiplier`: The symbol used to indicate multiplication 7 | /// - `thousandsep`: The separator between the thousands of the float. 8 | 9 | // str() converts minus "-" of a number to unicode "\u2212" 10 | value = str(value).replace("−", "-").replace(" ", "") //.replace(",", ".") 11 | 12 | let match-value = value.match(_re-num) 13 | assert.ne(match-value, none, message: "invalid number: " + value) 14 | let captures-value = match-value.captures 15 | 16 | let upper = none 17 | let lower = none 18 | if captures-value.at(14) != none { 19 | upper = captures-value.at(14) 20 | lower = none 21 | } else { 22 | upper = captures-value.at(5) 23 | lower = captures-value.at(7) 24 | } 25 | 26 | let formatted = _format-num( 27 | captures-value.at(0), 28 | exponent: captures-value.at(18), 29 | upper: upper, 30 | lower: lower, 31 | multiplier: multiplier, 32 | thousandsep: thousandsep, 33 | ) 34 | 35 | formatted = "$" + formatted + "$" 36 | eval(formatted) 37 | } 38 | 39 | #let add-unit(unit, shorthand, symbol, space: true) = { 40 | /// Add a new unit. 41 | /// - `unit`: Full name of the unit. 42 | /// - `shorthand`: Shorthand of the unit, usually only 1-2 letters. 43 | /// - `symbol`: String that will be inserted as the unit symbol. 44 | /// - `space`: Whether to put a space before the unit. 45 | context { 46 | let lang = _get-language() 47 | 48 | _lang-db.update(db => { 49 | db.at(lang).at("units").at(0).insert(unit, symbol) 50 | db.at(lang).at("units").at(1).insert(shorthand, symbol) 51 | db.at(lang).at("units").at(2).insert(unit, space) 52 | db.at(lang).at("units").at(3).insert(shorthand, space) 53 | db 54 | }) 55 | } 56 | } 57 | 58 | #let add-prefix(prefix, shorthand, symbol) = { 59 | /// Add a new prefix. 60 | /// - `prefix`: Full name of the prefix. 61 | /// - `shorthand`: Shorthand of the prefix, usually only 1-2 letters. 62 | /// - `symbol`: String that will be inserted as the prefix symbol. 63 | context { 64 | let lang = _get-language() 65 | 66 | _lang-db.update(db => { 67 | db.at(lang).at("prefixes").at(0).insert(prefix, symbol) 68 | db.at(lang).at("prefixes").at(1).insert(shorthand, symbol) 69 | db 70 | }) 71 | } 72 | } 73 | 74 | 75 | #let unit(unit, space: "#h(0.166667em)", per: "symbol") = { 76 | /// Format a unit. 77 | /// - `unit`: String containing the unit. 78 | /// - `space`: Space between units. 79 | /// - `per`: Whether to format the units after `per` or `/` with a fraction or exponent. 80 | 81 | context { 82 | let formatted-unit = "" 83 | formatted-unit = _format-unit(unit, space: space, per: per) 84 | 85 | let formatted = "$" + formatted-unit + "$" 86 | eval(formatted) 87 | } 88 | } 89 | 90 | #let qty( 91 | value, 92 | unit, 93 | rawunit: false, 94 | space: "#h(0.166667em)", 95 | multiplier: "dot", 96 | thousandsep: "#h(0.166667em)", 97 | per: "symbol", 98 | ) = { 99 | /// Format a quantity (i.e. number with a unit). 100 | /// - `value`: String containing the number. 101 | /// - `unit`: String containing the unit. 102 | /// - `multiplier`: The symbol used to indicate multiplication 103 | /// - `rawunit`: Whether to transform the unit or keep the raw string. 104 | /// - `space`: Space between units. 105 | /// - `thousandsep`: The separator between the thousands of the float. 106 | /// - `per`: Whether to format the units after `per` or `/` with a fraction or exponent. 107 | 108 | value = str(value).replace("−", "-").replace(" ", "") 109 | let match-value = value.match(_re-num) 110 | assert.ne(match-value, none, message: "invalid number: " + value) 111 | let captures-value = match-value.captures 112 | 113 | let upper = none 114 | let lower = none 115 | if captures-value.at(14) != none { 116 | upper = captures-value.at(14) 117 | lower = none 118 | } else { 119 | upper = captures-value.at(5) 120 | lower = captures-value.at(7) 121 | } 122 | 123 | let formatted-value = _format-num( 124 | captures-value.at(0), 125 | exponent: captures-value.at(18), 126 | upper: upper, 127 | lower: lower, 128 | multiplier: multiplier, 129 | thousandsep: thousandsep, 130 | ) 131 | 132 | context { 133 | let formatted-unit = "" 134 | if rawunit { 135 | formatted-unit = space + unit 136 | } else { 137 | formatted-unit = _format-unit(unit, space: space, per: per) 138 | } 139 | 140 | let formatted = "$" + formatted-value + formatted-unit + "$" 141 | eval(formatted) 142 | } 143 | } 144 | 145 | #let numrange( 146 | lower, 147 | upper, 148 | multiplier: "dot", 149 | delimiter: "-", 150 | space: "#h(0.16667em)", 151 | thousandsep: "#h(0.166667em)", 152 | ) = { 153 | /// Format a range. 154 | /// - `(lower, upper)`: Strings containing the numbers. 155 | /// - `multiplier`: The symbol used to indicate multiplication 156 | /// - `delimiter`: Symbol between the numbers. 157 | /// - `space`: Space between the numbers and the delimiter. 158 | /// - `thousandsep`: The separator between the thousands of the float. 159 | lower = str(lower).replace("−", "-").replace(" ", "") 160 | let match-lower = lower.match(_re-num) 161 | assert.ne(match-lower, none, message: "invalid lower number: " + lower) 162 | let captures-lower = match-lower.captures 163 | 164 | upper = str(upper).replace("−", "-").replace(" ", "") 165 | let match-upper = upper.match(_re-num) 166 | assert.ne(match-upper, none, message: "invalid upper number: " + upper) 167 | let captures-upper = match-upper.captures 168 | 169 | let formatted = _format-range( 170 | captures-lower.at(0), 171 | captures-upper.at(0), 172 | exponent-lower: captures-lower.at(18), 173 | exponent-upper: captures-upper.at(18), 174 | multiplier: multiplier, 175 | delimiter: delimiter, 176 | thousandsep: thousandsep, 177 | space: space, 178 | ) 179 | formatted = "$" + formatted + "$" 180 | 181 | eval(formatted) 182 | } 183 | 184 | #let qtyrange( 185 | lower, 186 | upper, 187 | unit, 188 | rawunit: false, 189 | multiplier: "dot", 190 | delimiter: "-", 191 | space: "", 192 | unitspace: "#h(0.16667em)", 193 | thousandsep: "#h(0.166667em)", 194 | per: "symbol", 195 | ) = { 196 | /// Format a range with a unit. 197 | /// - `(lower, upper)`: Strings containing the numbers. 198 | /// - `unit`: String containing the unit. 199 | /// - `rawunit`: Whether to transform the unit or keep the raw string. 200 | /// - `multiplier`: The symbol used to indicate multiplication 201 | /// - `delimiter`: Symbol between the numbers. 202 | /// - `space`: Space between the numbers and the delimiter. 203 | /// - `unitspace`: Space between units. 204 | /// - `thousandsep`: The separator between the thousands of the float. 205 | /// - `per`: Whether to format the units after `per` or `/` with a fraction or exponent. 206 | 207 | lower = str(lower).replace("−", "-").replace(" ", "") 208 | let match-lower = lower.match(_re-num) 209 | assert.ne(match-lower, none, message: "invalid lower number: " + lower) 210 | let captures-lower = match-lower.captures 211 | 212 | upper = str(upper).replace("−", "-").replace(" ", "") 213 | let match-upper = upper.match(_re-num) 214 | assert.ne(match-upper, none, message: "invalid upper number: " + upper) 215 | let captures-upper = match-upper.captures 216 | 217 | let formatted-value = _format-range( 218 | captures-lower.at(0), 219 | captures-upper.at(0), 220 | exponent-lower: captures-lower.at(18), 221 | exponent-upper: captures-upper.at(18), 222 | multiplier: multiplier, 223 | delimiter: delimiter, 224 | space: space, 225 | thousandsep: thousandsep, 226 | force-parentheses: true, 227 | ) 228 | 229 | context { 230 | let formatted-unit = "" 231 | if rawunit { 232 | formatted-unit = space + unit 233 | } else { 234 | formatted-unit = _format-unit(unit, space: unitspace, per: per) 235 | } 236 | 237 | let formatted = "$" + formatted-value + formatted-unit + "$" 238 | eval(formatted) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /money.csv: -------------------------------------------------------------------------------- 1 | bitcoin,btc,bitcoin,true 2 | dollar,usd,dollar,true 3 | euro,eur,euro,true 4 | franc,fr,franc,true 5 | lira,try,lira,true 6 | peso,peso,peso,true 7 | pound,gbp,pound,true 8 | ruble,rub,ruble,true 9 | rupee,inr,rupee,true 10 | won,krw,won,true 11 | yen,jpy,yen,true 12 | -------------------------------------------------------------------------------- /postfixes.csv: -------------------------------------------------------------------------------- 1 | squared,2 2 | cubed,3 -------------------------------------------------------------------------------- /prefixes-en.csv: -------------------------------------------------------------------------------- 1 | quecto,q,upright("q") 2 | ronto,r,upright("r") 3 | yocto,y,upright("y") 4 | zepto,z,upright("z") 5 | atto,a,upright("a") 6 | femto,f,upright("f") 7 | pico,p,upright("p") 8 | nano,n,upright("n") 9 | micro,u,upright("µ") 10 | milli,m,upright("m") 11 | centi,c,upright("c") 12 | deci,d,upright("d") 13 | deca,da,upright("da") 14 | hecto,h,upright("h") 15 | kilo,k,upright("k") 16 | mega,M,upright("M") 17 | giga,G,upright("G") 18 | tera,T,upright("T") 19 | peta,P,upright("P") 20 | exa,E,upright("E") 21 | zeta,Z,upright("Z") 22 | yotta,Y,upright("Y") 23 | ronna,R,upright("R") 24 | quetta,Q,upright("Q") 25 | kibi,Ki,upright("Ki") 26 | mebi,Mi,upright("Mi") 27 | gibi,Gi,upright("Gi") 28 | tebi,Ti,upright("Ti") 29 | pebi,Pi,upright("Pi") 30 | exbi,Ei,upright("Ei") 31 | zebi,Zi,upright("Zi") 32 | yobi,Yi,upright("Yi") 33 | -------------------------------------------------------------------------------- /prefixes-ru.csv: -------------------------------------------------------------------------------- 1 | quecto,q,upright("к") 2 | ronto,r,upright("р") 3 | yocto,y,upright("и") 4 | zepto,z,upright("з") 5 | atto,a,upright("а") 6 | femto,f,upright("ф") 7 | pico,p,upright("п") 8 | nano,n,upright("н") 9 | micro,u,upright("мк") 10 | milli,m,upright("м") 11 | centi,c,upright("с") 12 | deci,d,upright("д") 13 | deca,da,upright("да") 14 | hecto,h,upright("г") 15 | kilo,k,upright("к") 16 | mega,M,upright("М") 17 | giga,G,upright("Г") 18 | tera,T,upright("Т") 19 | peta,P,upright("П") 20 | exa,E,upright("Э") 21 | zeta,Z,upright("З") 22 | yotta,Y,upright("И") 23 | ronna,R,upright("Р") 24 | quetta,Q,upright("К") 25 | kibi,Ki,upright("Ки") 26 | mebi,Mi,upright("Ми") 27 | gibi,Gi,upright("Ги") 28 | tebi,Ti,upright("Ти") 29 | pebi,Pi,upright("Пи") 30 | exbi,Ei,upright("Эи") 31 | zebi,Zi,upright("Зи") 32 | yobi,Yi,upright("Йи") 33 | -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unify" 3 | version = "0.7.1" 4 | entrypoint = "lib.typ" 5 | authors = ["Christopher Hecker"] 6 | repository = "https://github.com/ChHecker/unify" 7 | license = "MIT" 8 | description = "Format numbers, units, and ranges correctly." 9 | keywords = ["numbers", "units", "ranges"] 10 | categories = ["text"] 11 | disciplines = [ 12 | "business", 13 | "chemistry", 14 | "computer-science", 15 | "economics", 16 | "engineering", 17 | "mathematics", 18 | "physics", 19 | ] 20 | exclude = ["examples"] 21 | -------------------------------------------------------------------------------- /units-en.csv: -------------------------------------------------------------------------------- 1 | meter,m,upright("m"),true 2 | metre,m,upright("m"),true 3 | lightyear,ly,upright("ly"),true 4 | parsec,pc,upright("pc"),true 5 | astronomicalunit,au,upright("au"),true 6 | angstrom,A,upright("Å"),true 7 | hectare,ha,upright("ha"),true 8 | barn,b,upright("b"),true 9 | litre,l,upright("l"),true 10 | liter,L,upright("L"),true 11 | second,s,upright("s"),true 12 | minute,min,upright("min"),true 13 | hour,h,upright("h"),true 14 | day,d,upright("d"),true 15 | year,a,upright("a"),true 16 | year,yr,upright("yr"),true 17 | hertz,Hz,upright("Hz"),true 18 | speedoflight,c,c,true 19 | gram,g,upright("g"),true 20 | ton,t,upright("t"),true 21 | tonne,t,upright("t"),true 22 | atomicmassunit,u,upright("u"),true 23 | newton,N,upright("N"),true 24 | dyne,dyn,upright("dyn"),true 25 | pascal,Pa,upright("Pa"),true 26 | atmosphere,atm,upright("atm"),true 27 | bar,bar,upright("bar"),true 28 | joule,J,upright("J"),true 29 | erg,erg,upright("erg"),true 30 | electronvolt,eV,upright("eV"),true 31 | watt,W,upright("W"),true 32 | ampere,A,upright("A"),true 33 | volt,V,upright("V"),true 34 | ohm,O,upright("Ω"),true 35 | siemens,S,upright("S"),true 36 | tesla,T,upright("T"),true 37 | gauss,G,upright("G"),true 38 | henry,H,upright("H"),true 39 | weber,Wb,upright("Wb"),true 40 | farad,F,upright("F"),true 41 | kelvin,K,upright("K"),true 42 | celsius,dC,upright(degree C),true 43 | fahrenheit,dF,upright(degree F),true 44 | candela,cd,upright("cd"),true 45 | lumen,lm,upright("lm"),true 46 | lux,lx,upright("lx"),true 47 | degree,deg,degree,false 48 | radian,r,upright("rad"),true 49 | radian,rad,upright("rad"),true 50 | arcminute,',',false 51 | arcsecond,\",″,false 52 | steradian,sr,upright("sr"),true 53 | mole,mol,upright("mol"),true 54 | molar,M,upright("M"),true 55 | becquerel,Bq,upright("Bq"),true 56 | gray,Gy,upright("Gy"),true 57 | sievert,Sv,upright("Sv"),true 58 | percent,%,percent,true 59 | decibel,dB,upright("dB"),true 60 | neper,Np,upright("Np"),true 61 | coulomb,C,upright("C"),true 62 | katal,kat,upright("kat"),true 63 | byte,B,upright("B"),true 64 | bit,b,upright("b"),true 65 | baud,Bd,upright("Bd"),true 66 | -------------------------------------------------------------------------------- /units-ru.csv: -------------------------------------------------------------------------------- 1 | meter,m,upright("м"),true 2 | metre,m,upright("м"),true 3 | lightyear,ly,upright("св.год."),true 4 | parsec,pc,upright("пк"),true 5 | astronomicalunit,au,upright("а.е."),true 6 | angstrom,A,upright("Å"),true 7 | hectar,ha,upright("га"),true 8 | litre,l,upright("л"),true 9 | liter,L,upright("Л"),true 10 | second,s,upright("с"),true 11 | minute,min,upright("мин"),true 12 | hour,h,upright("ч"),true 13 | day,d,upright("сут"),true 14 | year,a,upright("год"),true 15 | year,yr,upright("год"),true 16 | hertz,Hz,upright("Гц"),true 17 | gram,g,upright("г"),true 18 | ton,t,upright("т"),true 19 | tonne,t,upright("т"),true 20 | newton,N,upright("Н"),true 21 | pascal,Pa,upright("Па"),true 22 | atmosphere,atm,upright("атм"),true 23 | bar,bar,upright("бар"),true 24 | joule,J,upright("Дж"),true 25 | erg,erg,upright("эрг"),true 26 | electronvolt,eV,upright("еВ"),true 27 | watt,W,upright("Вт"),true 28 | ampere,A,upright("А"),true 29 | volt,V,upright("В"),true 30 | ohm,O,upright("Ом"),true 31 | siemens,S,upright("См"),true 32 | tesla,T,upright("Тл"),true 33 | gauss,G,upright("Гс"),true 34 | henry,H,upright("Гн"),true 35 | farad,F,upright("Ф"),true 36 | kelvin,K,upright("K"),true 37 | celsius,dC,upright(degree C),true 38 | fahrenheit,dF,upright(degree F),true 39 | candela,cd,upright("кд"),true 40 | lumen,lm,upright("лм"),true 41 | lux,lx,upright("лк"),true 42 | degree,d,degree,false 43 | radian,r,upright("рад"),true 44 | arcminute,',',false 45 | arcsecond,\",″,false 46 | mole,mol,upright("моль"),true 47 | becquerel,Bq,upright("Бк"),true 48 | gray,Gy,upright("Гр"),true 49 | sievert,Sv,upright("Зв"),true 50 | percent,%,percent,true 51 | decibel,dB,upright("дБ"),true 52 | neper,Np,upright("Нп"),true 53 | coulomb,C,upright("Кл"),true 54 | byte,B,upright("Б"),true 55 | baud,Bd,upright("бод"),true 56 | --------------------------------------------------------------------------------