Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ianmackenzie/elm-units
Simple, safe and convenient unit types and conversions for Elm
https://github.com/ianmackenzie/elm-units
elm hacktoberfest units
Last synced: 24 days ago
JSON representation
Simple, safe and convenient unit types and conversions for Elm
- Host: GitHub
- URL: https://github.com/ianmackenzie/elm-units
- Owner: ianmackenzie
- License: bsd-3-clause
- Created: 2018-07-20T18:32:14.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2023-06-15T15:44:06.000Z (over 1 year ago)
- Last Synced: 2024-12-16T20:52:54.028Z (27 days ago)
- Topics: elm, hacktoberfest, units
- Language: Elm
- Homepage: https://package.elm-lang.org/packages/ianmackenzie/elm-units/latest/
- Size: 421 KB
- Stars: 85
- Watchers: 8
- Forks: 14
- Open Issues: 19
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Authors: AUTHORS
Awesome Lists containing this project
- awesome-ccamel - ianmackenzie/elm-units - Simple, safe and convenient unit types and conversions for Elm (Elm)
README
# elm-units
_Release notes for 2.0 are [here](https://github.com/ianmackenzie/elm-units/releases/tag/2.0.0)._
`elm-units` is useful if you want to store, pass around, convert between,
compare, or do arithmetic on:- Durations (seconds, milliseconds, hours...)
- Angles (degrees, radians, turns...)
- Lengths (meters, feet, inches, miles, light years...)
- Temperatures (Celsius, Fahrenheit, kelvins)
- Pixels (whole or partial)
- Speeds (pixels per second, miles per hour...) or any other rate of change
- Any of the other built-in quantity types: areas, accelerations, masses,
forces, pressures, currents, voltages...
- Or even values in your own custom units, such as 'number of tiles' in a
tile-based gameIt is aimed especially at engineering/scientific/technical applications but is
designed to be generic enough to work well for other fields such as games and
finance. The core of the package consists of a generic `Quantity` type and
many concrete types such as `Length`, `Angle`, `Duration`, `Temperature`, and
`Speed`, which you can use to add some nice type safety to data types and
function signatures:```elm
type alias Camera =
{ fieldOfView : Angle
, shutterSpeed : Duration
, minimumOperatingTemperature : Temperature
}canOperateAt : Temperature -> Camera -> Bool
canOperateAt temperature camera =
temperature
|> Temperature.greaterThan
camera.minimumOperatingTemperature
```You can construct values of these types from any units you want, using provided
functions such as:```elm
Length.feet : Float -> Length
Length.meters : Float -> Length
Duration.seconds : Float -> Duration
Angle.degrees : Float -> Angle
Temperature.degreesFahrenheit : Float -> Temperature
```You can later convert back to plain numeric values, also in any units you want
(which do not have to be the same units used when initially constructing the
value!):```elm
Length.inCentimeters : Length -> Float
Length.inMiles : Length -> Float
Duration.inHours : Duration -> Float
Angle.inRadians : Angle -> Float
Temperature.inDegreesCelsius : Temperature -> Float
```This means that (among other things!) you can use these functions to do simple
unit conversions:```elm
Duration.hours 3 |> Duration.inSeconds
--> 10800Length.feet 10 |> Length.inMeters
--> 3.048Speed.milesPerHour 60 |> Speed.inMetersPerSecond
--> 26.8224Temperature.degreesCelsius 30
|> Temperature.inDegreesFahrenheit
--> 86
```Additionally, types like `Length` are actually type aliases of the form
`Quantity number units` (`Length` is `Quantity Float Meters`, for example,
meaning that it is internally stored as a number of meters), and there are
many generic functions which let you work directly with any kind of `Quantity`
values:```elm
Length.feet 3
|> Quantity.lessThan (Length.meters 1)
--> TrueDuration.hours 2
|> Quantity.plus (Duration.minutes 30)
|> Duration.inSeconds
--> 9000-- Some functions can actually convert between units!
-- Multiplying two Length values gives you an Area
Length.centimeters 60
|> Quantity.times
(Length.centimeters 80)
--> Area.squareMeters 0.48Quantity.sort
[ Angle.radians 1
, Angle.degrees 10
, Angle.turns 0.5
]
--> [ Angle.degrees 10 , Angle.radians 1 , Angle.turns 0.5 ]
```Ultimately, what this does is let you pass around and manipulate `Length`,
`Duration` or `Temperature` etc. values without having to worry about units.
When you initially construct a `Length`, you need to specify what units you're
using, but once that is done you can:- Store the length inside a data structure
- Pass it around between different functions
- Compare it to other lengths
- Add and subtract it to other lengths
- Multiply it by another length to get an area, or divide by a duration to
get a speed...and much more, all without having to care about units at all. All
calculations will be done in an internally consistent way, and when you finally
need to actually display a value on screen or encode to JSON, you can extract
the final result in whatever units you want.## Table of contents
- [Installation](#installation)
- [Usage](#usage)
- [Fundamentals](#fundamentals)
- [The `Quantity` type](#the-quantity-type)
- [Basic arithmetic and comparison](#basic-arithmetic-and-comparison)
- [Multiplication and division](#multiplication-and-division)
- [Argument order](#argument-order)
- [Custom functions](#custom-functions)
- [Custom units](#custom-units)
- [Understanding quantity types](#understanding-quantity-types)
- [Getting help](#getting-help)
- [API](#api)
- [Climate action](#climate-action)
- [Contributing](#contributing)
- [License](#license)## Installation
Assuming you have [installed Elm](https://guide.elm-lang.org/install.html) and
started a new project, you can install `elm-units` by running```
elm install ianmackenzie/elm-units
```in a command prompt inside your project directory.
## Usage
### Fundamentals
To take code that currently uses raw `Float` values and convert it to using
`elm-units` types, there are three basic steps:- Wherever you store a `Float`, such as in your model or in a message, switch
to storing a `Duration` or `Angle` or `Temperature` etc. value instead.
- Whenever you _have_ a `Float` (from an external package, JSON decoder etc.),
use a function such as `Duration.seconds`, `Angle.degrees` or
`Temperature.degreesFahrenheit` to turn it into a type-safe value.
- Whenever you _need_ a `Float` (to pass to an external package, encode as
JSON etc.), use a function such as `Duration.inMillliseconds`,
`Angle.inRadians` or `Temperature.inDegreesCelsius` to extract the value in
whatever units you want.
- Where you do math with `Float` values, switch to using `Quantity` functions
like `Quantity.plus` or `Quantity.greaterThan`. If this becomes impractical,
there are [other approaches](#custom-functions).### The `Quantity` type
All values produced by this package (with the exception of `Temperature`, which
is a bit of a special case) are actually values of type `Quantity`, defined as```elm
type Quantity number units
= Quantity number
```For example, `Length` is defined as
```elm
type alias Length =
Quantity Float Meters
```This means that a `Length` is internally stored as a `Float` number of `Meters`,
but the choice of internal units can mostly be treated as an implementation
detail.Having a common `Quantity` type means that it is possible to define generic
arithmetic and comparison operations that work on any kind of quantity; read on!### Basic arithmetic and comparison
You can do basic math with `Quantity` values:
```elm
-- 6 feet 3 inches, converted to meters
Length.feet 6
|> Quantity.plus (Length.inches 3)
|> Length.inMeters
--> 1.9050000000000002Duration.hours 1
|> Quantity.minus (Duration.minutes 15)
|> Duration.inMinutes
--> 45-- pi radians plus 45 degrees is 5/8 of a full turn
Quantity.sum [ Angle.radians pi, Angle.degrees 45 ]
|> Angle.inTurns
--> 0.625
````Quantity` values can be compared/sorted:
```elm
Length.meters 1 |> Quantity.greaterThan (Length.feet 3)
--> TrueQuantity.compare (Length.meters 1) (Length.feet 3)
--> GTQuantity.max (Length.meters 1) (Length.feet 3)
--> Length.meters 1Quantity.maximum [ Length.meters 1, Length.feet 3 ]
--> Just (Length.meters 1)Quantity.sort [ Length.meters 1, Length.feet 3 ]
--> [ Length.feet 3, Length.meters 1 ]
```### Multiplication and division
There are actually three different 'families' of multiplication and division
functions in the `Quantity` module, used in different contexts:- `multiplyBy` and `divideBy` are used to multiply (scale) or divide a
`Quantity` by a plain `Int` or `Float`, with `twice` and `half` for the common
cases of multiplying or dividing by 2
- `product`, `times`, `over` and `over_` are used to work with quantities that
are products of other quantities:
- multiply a `Length` by another `Length` to get an `Area`
- multiply an `Area` by a `Length` to get a `Volume`
- multiply a `Mass` by an `Acceleration` to get a `Force`
- divide a `Volume` by an `Area` to get a `Length`
- divide a `Force` by a `Mass` to get an `Acceleration`
- `rate`, `per`, `at`, `at_` and `for` are used to work with rates of change:
- divide `Length` by `Duration` to get `Speed`
- multiply `Speed` by `Duration` to get `Length`
- divide `Length` by `Speed` to get `Duration`
- And one bonus fourth function: `ratio`, used to divide two quantities with
the same units to get a plain `Float` valueFor example, to calculate the area of a triangle:
```elm
-- Area of a triangle with base of 2 feet and
-- height of 8 inches
base =
Length.feet 2height =
Length.inches 8Quantity.half (Quantity.product base height)
|> Area.inSquareInches
--> 96
```Comprehensive support is provided for working with rates of change:
```elm
-- How fast are we going if we travel 30 meters in
-- 2 seconds?
speed =
Length.meters 30 |> Quantity.per (Duration.seconds 2)-- How far do we go if we travel for 2 minutes
-- at that speed?
Duration.minutes 2 -- duration
|> Quantity.at speed -- length per duration
|> Length.inKilometers -- gives us a length!
--> 1.8-- How long will it take to travel 20 km
-- if we're driving at 60 mph?
Length.kilometers 20
|> Quantity.at_ (Speed.milesPerHour 60)
|> Duration.inMinutes
--> 12.427423844746679-- How fast is "a mile a minute", in kilometers per hour?
Length.miles 1
|> Quantity.per (Duration.minutes 1)
|> Speed.inKilometersPerHour
--> 96.56064-- Reverse engineer the speed of light from defined
-- lengths/durations (the speed of light is 'one light
-- year per year')
speedOfLight =
Length.lightYears 1
|> Quantity.per (Duration.julianYears 1)speedOfLight |> Speed.inMetersPerSecond
--> 299792458-- One astronomical unit is the (average) distance from the
-- Sun to the Earth. Roughly how long does it take light to
-- reach the Earth from the Sun?
Length.astronomicalUnits 1
|> Quantity.at_ speedOfLight
|> Duration.inMinutes
--> 8.316746397269274
```Note that the various functions above are not restricted to speed (length per
unit time) - any units work:```elm
pixelDensity =
Pixels.pixels 96 |> Quantity.per (Length.inches 1)Length.centimeters 3 -- length
|> Quantity.at pixelDensity -- pixels per length
|> Pixels.inPixels -- gives us pixels!
--> 113.38582677165354
```### Argument order
Note that several functions like `Quantity.minus` and `Quantity.lessThan` (and
their `Temperature` equivalents) that mimic binary operators like `-` and `<`
"take the second argument first"; for example,```elm
Quantity.lessThan x y
```means `y < x`, _not_ `x < y`. This is done for a couple of reasons. First, so
that use with `|>` works naturally; for example,```elm
x |> Quantity.lessThan y
```_does_ mean `x < y`. The 'reversed' argument order also means that things like
```elm
List.map (Quantity.minus x) [ a, b, c ]
```will work as expected - it will result in
```elm
[ a - x, b - x, c - x ]
```instead of
```elm
[ x - a, x - b, x - c ]
```which is what you would get if `Quantity.minus` took arguments in the 'normal'
order.There are, however, several functions that take arguments in 'normal' order, for
example:- `Quantity.difference` (compare to `minus`)
- `Quantity.product` (compare to `times`)
- `Quantity.rate` (compare to `per`)
- `Quantity.ratio`
- `Quantity.compare`In general the function names try to match how you would use them in English;
you would say "the difference of `a` and `b`" (and so `Quantity.difference a b`)
but "`a` minus `b`" (and so `a |> Quantity.minus b`).### Custom Functions
Some calculations cannot be expressed using the built-in `Quantity` functions.
Take kinetic energy `E_k = 1/2 * m * v^2`, for example - the `elm-units` type
system is not sophisticated enough to work out the units properly. Instead,
you'd need to create a custom function like```elm
kineticEnergy : Mass -> Speed -> Energy
kineticEnergy (Quantity m) (Quantity v) =
Quantity (0.5 * m * v^2)
```In the _implementation_ of `kineticEnergy`, you're working with raw `Float`
values so you need to be careful to make sure the units actually do work out.
(The values will be in [SI units][6] - meters, seconds etc.) Once the function
has been implemented, though, it can be used in a completely type-safe way -
callers can supply arguments using whatever units they have, and extract results
in whatever units they want:```elm
kineticEnergy (Mass.tonnes 1.5) (Speed.milesPerHour 60)
|> Energy.inKilowattHours
--> 0.14988357119999998
```### Custom Units
`elm-units` defines many standard unit types, but you can easily define your
own! See [CustomUnits][1] for an example.### Understanding quantity types
The same quantity type can often be expressed in multiple different ways. Take
the `Volume` type as an example. It is an alias for```elm
Quantity Float CubicMeters
```but expanding the `CubicMeters` type alias, this is equivalent to
```elm
Quantity Float (Cubed Meters)
```which expands further to
```elm
Quantity Float (Product (Product Meters Meters) Meters)
```which could also be written as
```elm
Quantity Float (Product (Squared Meters) Meters)
```or even
```elm
Quantity Float (Product SquareMeters Meters)
```and you may see any one of these forms pop up in compiler error messages.
## Getting Help
For general questions about using `elm-units`, try asking in the [Elm Slack][3]
or posting on the [Elm Discourse forums][4] or the [Elm subreddit][5]. I'm
**@ianmackenzie** on all three =)## API
[Full API documentation][10] is available.
## Climate action
I would like for the projects I work on to be as helpful as possible in
addressing the climate crisis. If- you are working on a project that helps address the climate crisis (clean
energy, public transit, reforestation, sustainable agriculture etc.) either as
an individual, as part of an non-profit organization or even as part of a
for-profit company, and
- there is a new feature you would find helpful for that work (or a bug you need
fixed) in any of my open-source projects, thenplease [open a new issue](https://github.com/ianmackenzie/elm-units/issues),
describe briefly what you're working on and I will treat that issue as high
priority.## Contributing
Yes please! One of the best ways to contribute is to add a module for a new
quantity type; see [issue #6][7] for details. I'll add a proper CONTRIBUTING.md
at some point, but some brief guidelines in the meantime:- Open a pull request by forking this repository, creating a new branch in
your fork, making all changes in that branch, then opening a pull request
from that branch.
- Format code with [`elm-format`][8] 0.8.1.
- Git commit messages should follow [the seven rules of a great Git commit
message][9], although I'm not strict about the 50 or 72 character rules.## License
[BSD-3-Clause © Ian Mackenzie][2]
[1]: https://github.com/ianmackenzie/elm-units/blob/master/doc/CustomUnits.md
[2]: https://github.com/ianmackenzie/elm-units/blob/master/LICENSE
[3]: http://elmlang.herokuapp.com/
[4]: https://discourse.elm-lang.org/
[5]: https://www.reddit.com/r/elm/
[6]: https://en.wikipedia.org/wiki/International_System_of_Units
[7]: https://github.com/ianmackenzie/elm-units/issues/6
[8]: https://github.com/avh4/elm-format
[9]: https://chris.beams.io/posts/git-commit/#seven-rules
[10]: https://package.elm-lang.org/packages/ianmackenzie/elm-units/latest