Skip to content

tokens-studio/unit-calculator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Unit Calculator

Use https://github.com/tokens-studio/tokenscript-interpreter for interpreting design tokens.

A tool to run calculations on strings with units.

The unit calculator lets you define custom rules on how mixed units will be resolved.

It will mainly be used to resolve math operations on design token epxressions.

Installation

npm i @tokens-studio/unit-calculator

Example

// Allows defining custom units
> 1h + 1min
[ "61min" ]

// Allows unit mixing defined per custom functions
> 10px + 1rem
[ "26px" ]

// Allows functions
> abs(2px)
[ "2px" ]

// Handles multiple expressions
> (2px * 4) - (2rem * 10) 1rem 2% * 10
[ "-312px", "1rem", "20%" ]

// Handles strings
> 10px - 1px solid green
[ "9px", "solid", "green" ]

Running it

In the console

npm run cli                           # Start a repl
npm run cli "1+1"                     # Evaluate expression
npm run cli --help                    # Show help

CLI Options

The CLI supports various configuration options:

# Control string handling
npm run cli --no-strings "1 + 2"
npm run cli --strings "hello world"

# Control multiple expressions
npm run cli --no-multiple-expressions "1 + 1 2 + 2"  # Will throw error
npm run cli --multiple-expressions "1 + 1 2 + 2"     # Returns [2, 4]

# Specify allowed units
npm run cli --units "px,em,rem" "1px + 2em"

# Combine options
npm run cli --no-strings --units "px,rem" "1px + 2rem"

CLI Help

npm run cli --help

Shows all available options:

  • --strings / --no-strings: Allow/disallow strings in expressions
  • --multiple-expressions / --no-multiple-expressions: Allow/disallow multiple expressions
  • --units <list>: Comma-separated list of allowed units
  • --help, -h: Show help message

Configuration

The configuration for the engine looks like this:

export interface CalcConfig {
  allowedUnits: Set<string>;
  mathFunctions: Record<string, args => number>;
  mathConstants: Record<string, number>;
  unitConversions: Map<string, (LeftToken, RightToken) => {value: number; unit: string | null}>;
}

allowedUnits

Define which units are allowed in your engine, per default CSS units are allowed.

Right now you can use any string value as unit to be allowed, any non allowed unit will throw.

For instance to allow measure units

{allowedUnits: new Set(["km", "m", "cm", "mm"])}

So you can use them in your calculations (once you've defined the conversion entries)

> 2km + 2km => [ "4km" ]

Undefined units will throw

> 2foo + 2bar
Invalid unit: "foo". Allowed units are: px, em, rem, %, vh, vw, vmin, vmax, cm, mm, in, pt, pc

unitConversions

You can define how units convert by passing in a Vector of rules.

These rules will define how the engine will convert units when you mix them.

Rules

  • Mixing the same units will always preserve units.
  • Unitless Numbers will be matched with null, e.g.: [null, '+', 'px']
  • You can give a wildcard operator with *, e.g.: ['*', '+', '%']

Defining a conversion table entry

To define an entry you give it a 3-tuple of [unit, operator, unit] and a function that handles the conversion.

For example if you want to convert rems when mixing with px by multiplying rem by a base size you could pass:

["px", "+", "rem"], (left, right) => {value: left.value + (right.value * 16), unit: "px"},

So you get this result

> 1px + 1rem
[ "17px" ]

Percent Example

You could define a unit % that will always add x% to the other value

export const createPercentConfig = function () {
  const addPercent = (percentToken, unitToken) => ({
    value: (unitToken.value / 100) * percentToken.value + unitToken.value,
    unit: unitToken.unit,
  });

  const config = createConfig();

  return addUnitConversions(config, [
    [
      ["%", "+", "*"], (left, right) => addPercent(left, right),
    ],
    [
      ["*", "+", "%"], (left, right) => addPercent(right, left),
    ],
  ]);
};

Now you can add percent to any value that you accept

> 100px + 10%
[ "110px" ]

mathFunctions

You can supply your own math functions via the mathFunctions property.

Per default all functions in js Math are included.

You could define custom functions like this

const options = {
    mathFunctions: { add: (a, b) => {value: a.value + b.value}, unit: a.unit },
};

Now you can use your custom functions like this

> add(10px, 10px)
[ "20px" ]

Unit mixing has to be custom handled in your function.

Concepts

This project demonstrates the fundamentals of a Pratt Parser. It's based on this repository, this paper by Vaughan Pratt, and also learns from this article and this article.

Parser

In general, the Pratt Parser solves the following problem: given the string "1 + 2 * 3", does the "2" associate with the "+" or the "*". It also solves "-" being both a prefix and infix operator, as well as elegantly handling right associativity.

The Pratt Parser is based on three computational units:

parser.expr(rbp)    // The expression parser
token.nud()         // "Null Denotation" (operates on no "left" context)
token.led(left, bp) // "Left Denotation" (operates with "left" context)

The parser.expr(rbp) function looks like:

function expr(rbp) {
  let left = lexer.next().nud()               // (1)
  while (rbp < lexer.peek().bp) {             // (2)
    const operator = lexer.next()           // (3)
    left = operator.led(left, operator.bp)  // (4)
  }
  return left
}

Of course, nud and led may recursively call expr.

The expr method can be summarized in english as "The loop (while) builds out the tree to the left, while recursion (led -> expr) builds the tree out to the right; nud handles prefix operators":

function expr(rbp) {
  // (1) handle prefix operator
  // (2) continue until I encounter an operator of lesser precedence than myself
  // (3) "eat" the operator
  // (4) give the operator the left side of the tree, and let it build the right side; this new tree is our new "left"
}

References

About

A calculation expression parser & evaluator accepting units and more

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •