Giter VIP home page Giter VIP logo

broch's Introduction

Clojars Project

Broch

A library for handling numbers with units.

Main Features:

  • Conversion, comparison and arithmetic.
  • Data literals
  • Clojurescript compatible
  • No dependencies

Named after Ole Jacob Broch for his contributions as director of the International Bureau of Weights and Measures.

Names

There is some disagreement on what to name things in this space, but I've settled on these definitions:

  • measure = the thing that is measured. (i.e :length or :time)
  • unit = a measure with a set scaling and a symbol. (the scaling is based in SI units)
  • quantity = a number with a unit.

Usage

The ergonomics for handling units is inspired by the excellent tick.core.

The following code assumes this ns definition.

(ns my-ns
  (:require [broch.core :as b]))

Basic units

; to turn a number into a quantity, use a unit fn
; there are many built-in ones, like b/meters
(b/meters 10) ;=> #broch/quantity[10 "m"]

; data literals
#broch/quantity[10 "m"] ;=> #broch/quantity[10 "m"]

; the unit fns also convert quantities, if compatible
(b/feet (b/meters 10)) ;=> #broch/quantity[32.8083989501 "ft"]

; but conversion of incompatible units throws an error
(b/meters (b/seconds 3)) ;=> ExceptionInfo "Cannot convert :time into :length"

; you can compare compatible units
(b/> #broch/quantity[1 "km"] #broch/quantity[999 "m"]) ;=> true

; and do arithmetic
(b/- #broch/quantity[1 "km"] #broch/quantity[1 "mi"]) ;=> #broch/quantity[-0.609344 "km"]

; and, again, we get sensible errors if incompatible
(b/- #broch/quantity[2 "km"] #broch/quantity[1 "s"]) ;=> ExceptionInfo "Cannot add/subtract :length and :time"

Derived units

; units have an internal composition-map of measure to exponent + scaling

; simple units just have their own measure
(b/composition (b/meters 2))
; => {:length 1, :broch/scaled 1}

; compound units have a more complicated composition map of the measures they're composed of
; as we all remember from school: W = J/s = N·m/s = kg·m²/s³
(b/composition (b/watts 4)) 
;=> {:mass 1, :length 2, :time -3, :broch/scaled 1}

; a kilowatt is the same, but scaled by 1000
(b/composition (b/kilowatts 4))
;=> {:mass 1, :length 2, :time -3, :broch/scaled 1000}

; this allows more complicated arithmetic (* and /) to derive the correct unit and convert the quantity, if it's defined
(b/* #broch/quantity[3 "m/s²"] #broch/quantity[3 "s"]) ;=> #broch/quantity[9 "m/s"]
(b/* #broch/quantity[12 "kW"] #broch/quantity[5 "h"]) ;=> #broch/quantity[60 "kWh"]
(b// #broch/quantity[12 "J"] #broch/quantity[1 "km"]) ;=> #broch/quantity[0.12 "N"]

; If all units are cancelled out, a number is returned
(b// #broch/quantity[1 "m"] #broch/quantity[2 "m"]) ;=> 1/2

; If no unit with a derived composition is defined, an error is thrown
(b// #broch/quantity[1 "m"] #broch/quantity[2 "s"]) ;=> #broch/quantity[0.5 "m/s"]
(b// #broch/quantity[2 "s"] #broch/quantity[1 "m"]) 
;=> ExceptionInfo "No derived unit is registered for {#broch/quantity[nil "s"] 1, #broch/quantity[nil "m"] -1}"

Defining new units

Broch comes with a bunch of units pre-defined in broch.core (more might be added in time).

But defining your own units is a peace-of-cake.

; all units have a measure and a symbol 
(b/measure #broch/quantity[1 "m"]) ;=> :speed 
(b/symbol #broch/quantity[1 "m"]) ;=> "m" (same as in the tag literal)

; broch uses the measure to know how to convert and derive units
; both must be given when defining the unit along with its scale from the "base" unit of that measure
(b/defunit meters :length "m" 1) ;=> #'my-ns/meters
(b/defunit seconds :time "s" 1) ;=> #'my-ns/seconds

; The built-in units rely on the SI-system for measures and their base units. 
; So the meter is the base unit of :length, and other units of :length must specify their scale relative to it. 
(b/defunit feet :length "ft" 0.3048) ;=> #'my-ns/feet  

; derived units are similar, but also take a unit-map giving their composition
(b/defunit meters-per-second :speed "m/s" {meters 1 seconds -1} 1) ;=> #'my-ns/meters-per-second
; Also note that since these units are already defined, running the `defunit` forms above would print warnings like 
; "WARN: a unit with symbol m already exists! Overriding..." 
; You probably don't want to override taken symbols, make up a new one instead.

; If you provide a composition, the given scaling is relative to the composing units
; so you could say for example:
; a yard is 0.9144 meters
(defunit yards :length "yd" 0.9144) :=> #'my-ns/yards 
; and a foot is a third of a yard
(defunit feet :length "ft" 1/3 {yards 1}) :=> #'my-ns/feet
; and there's 12 inches in a foot
(defunit inches :length "in" 1/12 {feet 1}) :=> #'my-ns/inches
; and it knows that an inch is scaled 0.0254 of a meter
(b/meters (b/inches 1)) ;=> #broch/quantity[0.0254 "m"]

; Measures and symbols are just names though, so they could be anything. For example:
(b/defunit me :coolness "me" 1) ;=> #'my-ns/me
(b/defunit rich-hickey :coolness "DJ Rich" 1000) ;=> #'my-ns/rich-hickey
(= #broch/quantity[1 "DJ Rich"] #broch/quantity[1000 "me"]) ;=> true

Tradeoffs

This library is not written for high performance. It rather tries to be as accurate as possible and avoid precision loss with floating-point numbers. This means that it sometimes "upcasts" numbers to ratios, if it cannot keep it as a double without losing precision. Ratios can be harder to read for humans, so where that is a concern you can cast the number type with b/boxed

(b/boxed double (b/meters 355/113)) ;=> #broch/quantity[3.141592920353982 "m"]

FAQ

Where's Celsius and Fahrenheit?

TLDR: Intentionally left out.

Both these common ways of denoting temperature has intentionally been left out of this library. This is because neither °C nor °F are actually just units of measure in the true sense, because their zero-points are not zero. They are units on a scale, which is why we prefix them with a °.

Zero grams is no mass, and zero miles per hour is no speed, but zero °C is not no temperature. It's quite a lot of temperature actually, exactly 273.15 K of temperature. Zero kelvin is no temperature, and that's why it is included in this library, and why it's (probably) the only unit for temperature you'll ever see used in any real computations involving temperature.

We could have added support for translation (shifting the zero-point), but that would have complicated conversion and raised some difficult questions on how to handle equality and arithmetic with these non-zero-based units.

For example:

; remember that 32°F = 0°C
(b/+ (b/fahrenheit 32) (b/celcius 0)) ;=> ?
(b/+ (b/celcius 0) (b/fahrenheit 32)) ;=> ?

Is it 0 °C or 64 °F? Both answers are plausible depending on which unit you choose to convert to before adding them together. And picking one interpretation, say always converting to the first argument's unit, would mean that b/+ and b/* are no longer commutative for temperatures, which is no good.

In conclusion, if you need to do stuff with temperatures and show the result in °C or °F, do whatever you need to do in kelvins and then scale the result yourself like this:

(def k #broch/quantity[345 "K"])

; to fahrenheit
(double
 (- (* (b/num k) 9/5)
    (rationalize 459.67))) 
;=> 161.33

; to celcius
(double
 (- (b/num k)
    (rationalize 273.15))) 
;=> 71.85
What happens when units have the same composition?

Some units are defined in terms of the same SI-units. For example, you have both Hertz and Becquerel defined as 1/s, but their measure is different. Hertz measures frequency and Becquerel measures radioactive decay, these measures differ in that one is periodic and the other isn't.

The problem is how to know which unit to use if a calculation returns a composition used by more than one unit, like 1/s.

(b// (b/meters-per-second 1) (b/meters 1)) ;=> hertz or becquerel?

Being correct in questions like this is outside the scope of this library. So in cases where the unit compositions are equal broch will just return the unit that was defined first as a default. In this case Hertz:

(b// (b/meters-per-second 1) (b/meters 1)) ;=> #broch/quantity[1 "Hz"]

And if you need an alternative unit you can explicitly convert it, as these units are compatible, despite having different measure , due to having equivalent compositions.

(b/becquerels (b/hertz 1)) ;=> #broch/quantity[1 "Bq"]

Deploy

To build jar and deploy to clojars:

  1. Have username and clojars-token in .deploy-opts.edn and run
  2. Run bin/deploy.bb
 bin/deploy.bb

broch's People

Contributors

2food avatar frozenlock avatar handerpeder avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

frozenlock

broch's Issues

Encoding / decoding

Encoding

Say you want to encode a unit as json. Given (meters 10), one reasonable representation is:

[10 , "m"]

another would be:

{
  "amount": 10,
  "unit": "m"
}

to do this you have to write something like:

(require '[anteo.units :as un])
(def m (un/meters 10))

[(un/num m) (anteo.units.impl/->symbol m)]

I think this is useful enought that anteo.units.impl/->symbol should be part of the public API.

Decoding

Say you receive the above json representation and want to construct a unit. Currently there is no good way to do this dynamically. As far as I can tell the only way to construct a meters unit is through the meters construct. So you would need to make a mapping somewhere:

{ "m" meters,
  "kg" kilograms,
  ,,,,; etc
}

I propose we create a constructor that creates a unit given an amount and its symbol. Something like:

(unit 10 "m")

would be equivalent to (meters 10) given that "m" is a registered symbol.

I propose we rename the current unit function to register-unit! and add a unit function as described above. unit doesn't currently have a two element arity so we could just add this to the existing function, but this might make the function confusing.

UOMs with differing zero points

This looks like a great library to help me with a project I need to complete.

I was wondering how you would represent a conversion between two units with differing zero points (e.g. Celsius to Fahrenheit) using this library?

Also, if a UOM conversion involves some lookup value to be used (e.g. going from volume to mass requires knowing the density of the liquid) is there a suggested approach for doing this.

Expose functions for custom arithmetic fn

I have a safe-divide function returning ##Inf when dividing by zero.

(defn safe-divide [a b]
  (if (zero? b)
    (if (neg? a)
      ##-Inf
      ##Inf)
    (/ a b)))

I'd like to have a version working with broch, but boxed-arithmetic only accepts a few functions from clojure.core.
I understand the logic behind that, but unfortunately writing my own custom boxed-arithmetic turns out to be quite difficult as most of the used functions are private.

Would it be possible to expose those functions?

Alternatively, perhaps boxed-arithmetic could expose two sub-functions where the user could do the dispatch manually?
Something like boxed-mult-div and boxed-add-sub-min-max.

Converting of nil quantity is undefined

nil quanities (like #broch/quantity[nil "m"]) should behave as if it's nil when used with broch fns.

For example conversion between units:

(b/nautical-miles (b/meters nil))

should yield #broch/quantity[nil "NM"], but now it throws an error.

CLJS and decimals

Hi, thanks for this useful library. However, there is one common problem with JS, precision.

CLJ environment:
(b/+ (b/millimeters 1) (b/meters 1)) => #broch/quantity[1001 "mm"]
(b/* (b/+ (b/millimeters 1) (-> (b/meters 1) (b/millimeters))) (b/meters 1.5)) => #broch/quantity[1501500 "mm²"]

CLJS environment
(b/+ (b/millimeters 1) (b/meters 1)) => #broch/quantity[1000.9999999999999 "mm"]
(b/* (b/+ (b/millimeters 1) (-> (b/meters 1) (b/millimeters))) (b/meters 1.5)) => #broch/quantity[1501499.9999999998 "mm²"]

e.g. https://mathjs.org/ doesn't have this problem.

I don't know what must be done to make this more consistent with JVM results. Maybe somebody thought about this already?

Thoughts on the future of currently unsupported units?

As of no.anteo/broch {:mvn/version "0.1.83"}, evaluating this expression:

(do
  (require '[broch.core :as b])
  (b/* (b/seconds 1) (b/seconds 1)))

Gives me an error:

1. Unhandled clojure.lang.ExceptionInfo
   No unit is registered for {:broch/scaled 1, :time 2}
   {:broch/scaled 1, :time 2}

What are your thoughts on currently unsupported units? Are you planning to stick to a selection of common units?

Units symbol not registered in CLJS

My existing code doesn't work in CLJS after updating from "2023.12.15" to "2024.02.27".
b/to-edn doesn't recognize unit symbols properly.
Screenshot 2024-02-27 at 10 43 18

Implementation of SQRT

Hi, I had to implement some functions, but unfortunately SQRT relies on the internal API.
What do you think about making them part of broch.core?

(defn pow [a b]
  (reduce b/* (repeat b a)))

(defn derive-sqrt-comp [x]
  (->> x
       b/composition
       (map (fn [[k v]] (if (= :broch/scaled k) 
                                     [k #?(:clj (rationalize (math/sqrt v))
                                         :cljs (math/sqrt v))] 
                                     [k (/ v 2)])))
       (into {})))
       
(defn sqrt [x]
  (let [result (math/sqrt (b/num x))
        composition (derive-sqrt-comp x)
        new-unit (get @broch.impl/composition-registry composition)]
    (when new-unit
      (broch.impl/quantity new-unit result))))

UOM conversion that requires a lookup

This looks like a great library to help me with a project I need to complete.

If a UOM conversion involves some lookup value to be used (e.g. going from volume to mass requires knowing the density of the liquid) is there a suggested approach for doing this?

Clojure-LSP doesn't seem to find `broch.core/meters`

Hi!

My clojure-lsp setup doesn't seem to be able to find the definition for broch.core/meters (and more) variables.

broch.core/meters works perfectly in the REPL. And broch.core/meters looks like a normal def, so I'd expect this to just work.

This is what I'm seeing, lint error for broch.core/meters and no completion for meters (but completion for other symbols!).

image image

Broch version:

        no.anteo/broch {:mvn/version "0.1.83"}

Screenshots were produced with this LSP+clj-kondo version:

 :server-version "2023.08.06-00.33.37-nightly",
 :clj-kondo-version "2023.07.14-SNAPSHOT",

(up-to-date versions provided by Doom Emacs (clojure +lsp) as of 2023-10-19).


For all that I can see, this is more likely to indicate an issue in Clojure-LSP than broch. But I thought I'd ask if you guys have any ideas first -- because you know the broch codebase.

I think it's weird that some vars (like b/* og b/max) were found without issues, whereas other vars (like b/meters) were not found. Perhaps a first step is to try reproduce the issue.

Teodor

Converts to ratio

Numbers are sometimes converted to ratios on unit construction. Like this

(b/meters 301873.55787421204) 
; => #broch/unit[7546838946855301/25000000000 "m"]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.