Skip to content

Repository files navigation

Measured Build Status

Encapsulates measurements with their units. Provides easy conversion between units. Built in support for weight, length, and volume.

Lightweight and easily extensible to include other units and conversions. Conversions done with Rational for precision.

Since version 3.0.0, the adapter to integrate measured with Ruby on Rails is also a part of this gem. If you had been using measured-rails for that functionality, you should now remove measured-rails from your gem file.

Installation

Using bundler, add to the Gemfile:

gem'measured'

Or stand alone:

$ gem install measured

Usage

Initialize a measurement:

Measured::Weight.new("12","g")
> #<Measured::Weight: 12 #<Measured::Unit: g (gram, grams)>>

Convert to return a new measurement:

Measured::Weight.new("12","g").convert_to("kg")
> #<Measured::Weight: 0.012 #<Measured::Unit: kg (kilogram, kilograms) 1000/1 g>>

Agnostic to symbols/strings:

Measured::Weight.new(1,"kg") == Measured::Weight.new(1,:kg)
> true

Seamlessly handles aliases:

Measured::Weight.new(12,:oz) == Measured::Weight.new("12",:ounce)
> true

Raises on unknown units:

beginMeasured::Weight.new(1,:stone)rescueMeasured::UnitErrorputs"Unknown unit"end

Parse from string without having to split out the value and unit first:

Measured::Weight.parse("123 grams")
> #<Measured::Weight: 123 #<Measured::Unit: g (gram, grams)>>

Parse can scrub extra whitespace and split number from unit:

Measured::Weight.parse(" 2kg ")
> #<Measured::Weight: 2 #<Measured::Unit: kg (kilogram, kilograms) 1000/1 g>>

Perform addition / subtraction against other units, all represented internally as Rational or BigDecimal:

Measured::Weight.new(1,:g) + Measured::Weight.new(2,:g)
> #<Measured::Weight: 3 #<Measured::Unit: g (gram, grams)>>Measured::Weight.new("2.1",:g) - Measured::Weight.new(1,:g)
> #<Measured::Weight: 1.1 #<Measured::Unit: g (gram, grams)>>

Multiplication and division by units is not supported, but the actual value can be scaled by a scalar:

Measured::Weight.new(10,:g).scale(0.5)
> #<Measured::Weight: 5 #<Measured::Unit: g (gram, grams)>>Measured::Weight.new(2,:g).scale(3)
> #<Measured::Weight: 6 #<Measured::Unit: g (gram, grams)>>

In cases of differing units, the left hand side takes precedence:

Measured::Weight.new(1000,:g) + Measured::Weight.new(1,:kg)
> #<Measured::Weight: 2000 #<Measured::Unit: g (gram, grams)>>

Converts units only as needed for equality comparison:

> Measured::Weight.new(1000,:g) == Measured::Weight.new(1,:kg)true

Extract the unit and the value:

weight=Measured::Weight.new("1.2","grams")weight.value
> #<BigDecimal:7fabf6c1d0a0,'0.12E1',18(18)>weight.unit
> #<Measured::Unit: g (gram, grams)>

See all valid units:

Measured::Weight.unit_names
> ["g","kg","lb","oz"]

Check if a unit is a valid unit or alias:

Measured::Weight.unit_or_alias?(:g)
> trueMeasured::Weight.unit_or_alias?("gram")
> trueMeasured::Weight.unit_or_alias?("stone")
> false

See all valid units with their aliases:

Measured::Weight.unit_names_with_aliases
> ["g","gram","grams","kg","kilogram","kilograms","lb","lbs","ounce","ounces","oz","pound","pounds"]

String formatting:

Measured::Weight.new("3.14","grams").format("%.1<value>f %<unit>s")
> "3.1 g"

If no string is passed to the format method it defaults to "%.2<value>f %<unit>s".

If the unit isn't the standard SI unit, it will include a conversion string.

Measured::Weight.new("3.14","kg").format
> "3.14 kg (1000/1 g)"Measured::Weight.new("3.14","kg").format(with_conversion_string: false)
> "3.14 kg"

Active Record

This gem also provides an Active Record adapter for persisting and retrieving measurements with their units, and model validations.

Columns are expected to have the _value and _unit suffix, and be DECIMAL and VARCHAR, and defaults are accepted. Customizing the column used to hold units is supported, see below for details.

classAddWeightAndLengthToThings < ActiveRecord::Migrationdefchangeadd_column:things,:minimum_weight_value,:decimal,precision: 10,scale: 2add_column:things,:minimum_weight_unit,:string,limit: 12add_column:things,:total_length_value,:decimal,precision: 10,scale: 2,default: 0add_column:things,:total_length_unit,:string,limit: 12,default: "cm"endend

A column can be declared as a measurement with its measurement subclass:

classThing < ActiveRecord::BasemeasuredMeasured::Weight,:minimum_weightmeasuredMeasured::Length,:total_lengthmeasuredMeasured::Volume,:total_volumeend

You can optionally customize the model's unit column by specifying it in the unit_field_name option, as follows:

classThingWithCustomUnitAccessor < ActiveRecord::Basemeasured_length:length,:width,:height,unit_field_name: :size_unitmeasured_weight:total_weight,:extra_weight,unit_field_name: :weight_unitmeasured_volume:total_volume,:extra_volume,unit_field_name: :volume_unitend

Similarly, you can optionally customize the model's value column by specifying it in the value_field_name option, as follows:

classThingWithCustomValueAccessor < ActiveRecord::Basemeasured_length:length,value_field_name: :custom_lengthmeasured_weight:total_weight,value_field_name: :custom_weightmeasured_volume:volume,value_field_name: :custom_volumeend

There are some simpler methods for predefined types:

classThing < ActiveRecord::Basemeasured_weight:minimum_weightmeasured_length:total_lengthmeasured_volume:total_volumeend

This will allow you to access and assign a measurement object:

thing=Thing.newthing.minimum_weight=Measured::Weight.new(10,"g")thing.minimum_weight_unit# "g"thing.minimum_weight_value# 10

Order of assignment does not matter, and each property can be assigned separately and with mass assignment:

params={total_length_unit: "cm",total_length_value: "3"}thing=Thing.new(params)thing.total_length.to_s# 3 cm

Validations

Validations are available:

classThing < ActiveRecord::Basemeasured_length:total_lengthvalidates:total_length,measured: trueend

This will validate that the unit is defined on the measurement, and that there is a value.

Rather than true the validation can accept a hash with the following options:

  • message: Override the default "is invalid" message.
  • units: A subset of units available for this measurement. Units must be in existing measurement.
  • greater_than
  • greater_than_or_equal_to
  • equal_to
  • less_than
  • less_than_or_equal_to

All comparison validations require Measured::Measurable values, not scalars. Most of these options replace the numericality validator which compares the measurement/method name/proc to the column's value. Validations can also be combined with presence validator.

Note: Validations are strongly recommended since assigning an invalid unit will cause the measurement to return nil, even if there is a value:

thing=Thing.newthing.total_length_value=1thing.total_length_unit="invalid"thing.total_length# nil

Units and conversions

SI units support

There is support for SI units through the use of si_unit. Units declared through it will have automatic support for all SI prefixes:

Multiplying FactorSI PrefixScientific Notation
1 000 000 000 000 000 000 000 000yotta (Y)10^24
1 000 000 000 000 000 000 000zetta (Z)10^21
1 000 000 000 000 000 000exa (E)10^18
1 000 000 000 000 000peta (P)10^15
1 000 000 000 000tera (T)10^12
1 000 000 000giga (G)10^9
1 000 000mega (M)10^6
1 000kilo (k)10^3
0.001milli (m)10^-3
0.000 001micro (µ)10^-6
0.000 000 001nano (n)10^-9
0.000 000 000 001pico (p)10^-12
0.000 000 000 000 001femto (f)10^-15
0.000 000 000 000 000 001atto (a)10^-18
0.000 000 000 000 000 000 001zepto (z)10^-21
0.000 000 000 000 000 000 000 001yocto (y)10^-24

Bundled unit conversion

  • Measured::Weight
    • g, gram, grams, and all SI prefixes
    • t, metric_ton, metric_tons
    • slug, slugs
    • N, newtons, newton
    • long_ton, long_tons, weight_ton, weight_tons, 'W/T', imperial_ton, imperial_tons, displacement_ton, displacement_tons
    • short_ton, short_tons
    • lb, lbs, pound, pounds
    • oz, ounce, ounces
  • Measured::Length
    • m, meter, metre, meters, metres, and all SI prefixes
    • in, inch, inches
    • ft, foot, feet
    • yd, yard, yards
    • mi, mile, miles
  • Measured::Volume
    • l, liter, litre, liters, litres, and all SI prefixes
    • m3, cubic_meter, cubic_meters, cubic_metre, cubic_metres
    • ft3, cubic_foot, cubic_feet
    • in3, cubic_inch, cubic_inches
    • gal, imp_gal, imperial_gallon, imp_gals, imperial_gallons
    • us_gal, us_gallon, us_gals, us_gallons
    • qt, imp_qt, imperial_quart, imp_qts, imperial_quarts
    • us_qt, us_quart, us_quarts
    • pt, imp_pt, imperial_pint, imp_pts, imperial_pints
    • us_pt, us_pint, us_pints
    • oz, fl_oz, imp_fl_oz, imperial_fluid_ounce, imperial_fluid_ounces
    • us_oz, us_fl_oz, us_fluid_ounce, us_fluid_ounces

You can skip these and only define your own units by doing:

gem'measured',require: 'measured/base'

Shortcut syntax

There is a shortcut initialization syntax for creating instances of measurement classes that can avoid the .new:

Measured::Weight(1,:g)
> #<Measured::Weight: 1 #<Measured::Unit: g (gram, grams)>>

Adding new units

Extending this library to support other units is simple. To add a new conversion, use Measured.build to define your base unit and conversion units:

Measured::Thing=Measured.builddounit:base_unit,# Add a unit to the systemaliases: [:bu]# Allow it to be aliased to other names/symbolsunit:another_unit,# Add a second unit to the systemaliases: [:au],# All units allow aliases, as long as they are uniquevalue: "1.5 bu"# The conversion rate to another unitend

All unit names are case sensitive.

Values for conversion units can be defined as a string with two tokens "number unit" or as an array with two elements. All values will be parsed as / coerced to Rational. Conversion paths don't have to be direct as a conversion table will be built for all possible conversions.

Namespaces

All units and classes are namespaced by default, but can be aliased in your application.

Weight=Measured::WeightLength=Measured::LengthVolume=Measured::Volume

Alternatives

Existing alternatives which were considered:

  • Pros
    • Accurate math and conversion factors.
    • Includes nearly every unit you could ask for.
  • Cons
    • Opens up and modifies Array, Date, Fixnum, Math, Numeric, String, Time, and Object, then depends on those changes internally.
    • Lots of code to solve a relatively simple problem.
    • No Active Record adapter.
  • Pros
    • Lightweight.
  • Cons
    • All math done with floats making it highly lossy.
    • All units assumed to be pluralized, meaning using unit abbreviations is not possible.
    • Not actively maintained.
    • No Active Record adapter.
  • Pros
    • Well written.
    • Conversions done with Unified Code for Units of Measure (UCUM) so highly accurate and reliable.
  • Cons
    • Lots of code. Good code, but lots of it.
    • Many modifications to core types.
    • Active Record adapter exists but is written and maintained by a different person/org.
    • Not actively maintained.

Contributing

  1. Fork it ( https://github.com/Shopify/measured/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Authors

About

Encapsulate measurements and their units in Ruby and Ruby on Rails.

Resources

Code of conduct

Security policy

Stars

383 stars

Watchers

422 watching

Forks

Releases

Packages

Used by

Contributors

Languages