Fancy-units - format numbers and units with style

If you are thinking “why would we possibly need another units package?”, I can assure you that fancy-units actually brings something new to the table. While the existing packages are just aiming to be ports of siunitx, I tried to design the package to work/feel like native Typst. The input of numbers and units is entirely content-based, and you can use this to style (parts) of the numbers and units with the built-in Typst functions. There are also no unit macros necessary, you can just write down the units like regular text, and by default the decimal separator is directly inferred from the text language.

A few examples

Configuration

There are a few options to change the output format of numbers and units. Most importantly the uncertainty-mode to use absolute or relative uncertainties, and the per-mode to use regular fractions, in-line fractions or powers. For the full documentation, please see the manual.pdf


You can change these options through a state for an entire document/scope

#import "@preview/fancy-units:0.1.0": fancy-units-configure

#fancy-units-configure(
  uncertainty-mode: "plus-minus",
  per-mode: "slash",
  ...
)

or individually for each number and unit

#import "@preview/fancy-units:0.1.0": num, unit

#num(uncertainty-mode: "parentheses")[0.9(1)]
#unit(per-mode: "power")[W / kg]

Where to start?

If you are curious by now and want to play around with the package, you can use the following examples to get started

#import "@preview/fancy-units:0.1.0": fancy-units-configure, num, unit, qty

#fancy-units-configure(
  uncertainty-mode: "conserve",
  per-mode: "fraction"
)

$
  #num[123*(4)*e5] \
  #unit[kg m / s^2] \
  #qty[137][#text(red)[μ]:m] \
  #qty[1][M:#sym.Omega] \
  #unit(per-mode: "power")[kg / m^-2] \
  #unit[F] = #unit(per-mode: "fraction", unit-separator: sym.dot)[(s C)^2 / (m^2 kg)] \
  #unit[kg#math.cancel[^2] / #math.cancel[kg]] = #unit[kg]
$

Planned features

So far I am still planning to implement the following features:

  • Macros for custom units (I am already getting annoyed by having to type _E_#sub[rec] all the time…)
  • Support more built-in (styling) functions
  • Digit grouping to make long numbers easier to read (299 792 458 instead of 299792458)

If you have any suggestions or ideas how I could further improve the package, please let me know. :slight_smile:

15 Likes

This is really neat! I was wishing someone made something like this, I especially like the “styling” options. I’m sure there’s ways to add styling options like x10 vs E notation, truncating some long large numbers like this: 1296196107 => 1.296*10…, automatically converting cm based units to m, etc etc. I really like the package, but I’d recomend to take a look at other unit packages and see if you can merge your code into theirs, or theirs into yours. Most packages have very permissible licenses and it would be a bummer to have a fragmented ecosystem.

1 Like

Thank you for your feedback!

I of course looked at the existing unit packages unify and metro before deciding to work on this package. At the time, neither package supported all the features I needed for my own work (uncertainties and custom units). There were ultimately two reasons for me two create a new package:

  1. I wanted to see if it is possible to break with the convention of predefined units from siunitx. (The configuration of the output format is still similar to siunitx, but I have some ideas how this could be changed as well.) It is of course nice to have a similar user experience, but “just” porting a LaTeX package seems to miss the point of Typst in my opinion (This is not supposed to belittle the work of the authors of the other packages, I’m just trying to make a point.) If there could be a better solution, why should we not try to find it?
  2. I used this project to learn (about) Typst in preparation for writing my thesis. This is of course a personal reason and it would certainly have been more efficient to just contribute to the existing packages. But in this case I did not care about efficiency. And since the ecosystem was already fragmented before, I don’t feel too bad about contributing to the fragmentation.

On a personal note, I don’t think having multiple packages with different approaches is a bad thing. You could argue that the naming convention for packages even encourages this. As long as Typst is still in beta, none of the packages can be considered “final” anyway. I’m certainly excited for custom types and how they will impact the package ecosystem, even if that causes further fragmentation. :slight_smile:

2 Likes

Hey janek, we are currently writing some chemistry packages, and one will be about showing chemical units. It’s not going to be “yet another units package”, since we will be using another package (such as fancy units) for displaying everything under the hood, while our package is just going to take care of converting mol to grams, millilitres, concentrations, etc. so more of a focus on calculating than formatting as well as working with variables, etc.
I’m thinking about using fancy units vs unify and not sure which one would be best suited. I can see that you’re actively developing fancy units and I was wondering if you would be interested in adding third party support similar to how zero does it, i.e. have a more performant API that skips all the validation and interpretation logic and lets other tools just input the raw data and returns content that can be placed by the third party package. As an example of what we’re trying to do:

#let zinc-chloride = get-molecule("ZnCl2")
Add #grams(zinc-chloride, mol:0.1) to 100ml of water

output:
Add 13.6315 g ZnCl₂ to 100ml of water.

So it uses the data we have and converts the units automatically, while giving users all the styling options so they can define presets for the entire document or specific invocations. It would automatically use kg or mg depending on how much of the chemical you want, etc.

1 Like

Hey, thank you for the question! I only started working on version 0.2 a few days ago but I can already share what I am planning with the project. This should hopefully answer your question about third-party support and general customizability.

I only included the essential customization options in version 0.1 because I was hoping to find a better solution to customize the numbers and units. Introducing a parameter for every possible option is obviously the easiest solution, but I don’t think this scales very well. And there will always be someone who wants some obscure formatting option that might need another parameter. If you have ever looked at the option tables in the siuntix manual, you will know what I am talking about.

My plan for version 0.2 is therefore to drop the individual parameters and use functions instead. There will be two categories, “transform” and “format”. Functions in the former category will allow you to change some aspect of the number or unit, e.g. convert the uncertainties to the “absolute” format or round the value. Functions in the latter category will then actually turn the dictionaries into content. The base functions num() and unit() will only have the named positional parameters transform and format and the positional parameter body.

#let num(transform: auto, format: auto, body) = { ... }
#let unit(transform: auto, format: auto, body) = { ... }

If you want to run multiple transformation, you can pass a list of functions that will be executed one after the other. The same works for the formatting if the functions only format parts of the number (or unit).
Let’s say you would like to round your values to three decimal digits and you want to use absolute uncertainties. And for the exponent you would like use sym.dot instead of sym.times as the separator between the base (10) and the value (and uncertainties). The configuration to achieve this would look like this:

#configure(
  num-transform: (
    round.with(n: 3),
    absolute-uncertainties,
  ),
  num-format: (
    format-exponent.with(separator: sym.dot),
    format-num,
  )
)

If you want to change your number or unit in a way that is not (yet) supported by a function in the package, you can then just write your own function(s). The function signature and (schema of) the return value will be the only restrictions.

Regarding your request of using the formatting (and transformation) without the validation and interpretation I want to allow dictionaries as the body for num(), unit() and qty(). The internal functions interpret-num() and interpret-unit() are then just skipped based on the input type. I am not sure yet how restrictive the input format for the dictionaries should be. I think it would be nice if you could just write

num((value: decimal("0.9")))

instead of

num((value: (body: decimal("0.9"), layers: ()), uncertainties: (), exponent: none))

in case you don’t want to use uncertainties, an exponent, or any of the styling options. On the other hand it might be simpler to just support a single format for third-party access to avoid any confusion. I would definitely be interested in hearing your opinion on that!

The last important change will be to use the decimal type instead of strings for any values that are actually a number. This was unfortunately not available when I started working on the project and I didn’t want to worry about it before the initial release since it is only used internally.

I am looking forward to your comments (if you have any) and I hope that I was able to convince you that fancy-units will be the right choice for your chemical units. :grin:

hmm, if you are using something like configure, wouldn’t that mean that you thereby are using states? In my opinion it’s best to reduce the amount of context you’re using in the document, both because of performance reasons and because it makes the content opaque from the outside. Maybe it would make more sense to advise people who want to customise things use it like this:

#let num = num.with(transform: ... , format: ... )

which would have the same effect as configure and you can still change it multiple times in the document without needing to use state.

Apart from that I really like this approach, it definitely feels appropriate and “typst-y”.
I think we would mostly be interested in the quantity method, since we will be displaying both numbers with units as well as a descriptor (i.e. 1m of rope, 100ml of water, 10g of iron) and would just handle the “of …” part ourselves, including localisation. I honestly don’t have a preference for the restrictive input style dictionaries or the more verbose one. most important would be that we can bubble up any customisability to users of our package in the same way users of fancy units would, so that if they were to use #num and #mol in the same paragraph they can style them the same way. Ideally we would just like to be a thin chemistry related wrapper of fancy units.

This was a good read as well: Types and Context | Laurenz's Blog
I think it’s from one of the core developers of typst

I would be using states internally. One for the configuration and one for macros. They are only used if anything is set to auto though. You could definitely redefine the function num() in the way you showed above. And if you want to have e.g. a “rounding” num that you use occasionally, defining a shortcut

#let rnum = num.with(transform: (round.with(n: 3), auto))

would even be the recommended way. This would however only get rid of the state.get() calls, the context would still be used. Do you actually know if there is any information available on the performance downside of using context and states? Or is the best way to just create some dummy document and to measure the compilation time?

If you want any customization to apply to both the function mol() in your chemistry package and the function num() (or rather qty()) from fancy-units, this can only done with a state, right? The function mol() would not be aware of the user defining

#let qty = qty.with(num-transform: group-digits, num-format: format-num.with(decimal-separator: ","))

since the function qty() is imported directly from the package fancy-units inside your chemistry package. Or am I missing something here and there is some hidden magic behind importing that I am not yet aware of?

According to typst documentation:

To resolve contextual interactions, the Typst compiler processes your document multiple times. […] In certain cases, Typst may even need more than two iterations to resolve everything. While that’s sometimes a necessity, it may also be a sign of misuse of contextual functions (e.g. of state). If Typst cannot resolve everything within five attempts, it will stop and output the warning “layout did not converge within 5 attempts.”

So there’s that I guess. I’ve seen a bunch of convos saying that context was the main thing slowing down things, but I think there’s plans to change the architecture(?) For now I’d just say be careful about it. Also there’s this in the typst community guidelines: guidelines/src/chapters/api/flexibility.typ at main · typst-community/guidelines · GitHub
and they’re basically saying that because it makes content opaque it it makes downstream usage of it harder. But I also disagree with the suggestion to require users to use context, just makes things more verbose, especially for something as commonly used as units…

I don’t think it would be possible right now to make any customisation to qty automatically apply to mol, but I think it’s good enough to allow the same options, so people can just set each one up in the same way, right? doing #let qty = qty.with() would also leave num unaffected, right? so you would need to do a customization for each function even when using functions from the same package. I feel like mol wouldn’t look out of place then, and the top of my file is full with a bunch of setup and customisation anyways…