Git Product home page Git Product logo

measured's Introduction

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:

begin
  Measured::Weight.new(1, :stone)
rescue Measured::UnitError
  puts "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)
> true
Measured::Weight.unit_or_alias?("gram")
> true
Measured::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.

class AddWeightAndLengthToThings < ActiveRecord::Migration
  def change
    add_column :things, :minimum_weight_value, :decimal, precision: 10, scale: 2
    add_column :things, :minimum_weight_unit, :string, limit: 12

    add_column :things, :total_length_value, :decimal, precision: 10, scale: 2, default: 0
    add_column :things, :total_length_unit, :string, limit: 12, default: "cm"
  end
end

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

class Thing < ActiveRecord::Base
  measured Measured::Weight, :minimum_weight
  measured Measured::Length, :total_length
  measured Measured::Volume, :total_volume
end

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

class ThingWithCustomUnitAccessor < ActiveRecord::Base
  measured_length :length, :width, :height,     unit_field_name: :size_unit
  measured_weight :total_weight, :extra_weight, unit_field_name: :weight_unit
  measured_volume :total_volume, :extra_volume, unit_field_name: :volume_unit
end

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

class ThingWithCustomValueAccessor < ActiveRecord::Base
  measured_length :length, value_field_name: :custom_length
  measured_weight :total_weight, value_field_name: :custom_weight
  measured_volume :volume, value_field_name: :custom_volume
end

There are some simpler methods for predefined types:

class Thing < ActiveRecord::Base
  measured_weight :minimum_weight
  measured_length :total_length
  measured_volume :total_volume
end

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

thing = Thing.new
thing.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:

class Thing < ActiveRecord::Base
  measured_length :total_length

  validates :total_length, measured: true
end

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.new
thing.total_length_value = 1
thing.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 Factor SI Prefix Scientific Notation
1 000 000 000 000 000 000 000 000 yotta (Y) 10^24
1 000 000 000 000 000 000 000 zetta (Z) 10^21
1 000 000 000 000 000 000 exa (E) 10^18
1 000 000 000 000 000 peta (P) 10^15
1 000 000 000 000 tera (T) 10^12
1 000 000 000 giga (G) 10^9
1 000 000 mega (M) 10^6
1 000 kilo (k) 10^3
0.001 milli (m) 10^-3
0.000 001 micro (µ) 10^-6
0.000 000 001 nano (n) 10^-9
0.000 000 000 001 pico (p) 10^-12
0.000 000 000 000 001 femto (f) 10^-15
0.000 000 000 000 000 001 atto (a) 10^-18
0.000 000 000 000 000 000 001 zepto (z) 10^-21
0.000 000 000 000 000 000 000 001 yocto (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.build do
  unit :base_unit,           # Add a unit to the system
    aliases: [:bu]           # Allow it to be aliased to other names/symbols

  unit :another_unit,        # Add a second unit to the system
    aliases: [:au],          # All units allow aliases, as long as they are unique
    value: "1.5 bu"        # The conversion rate to another unit
end

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::Weight
Length = Measured::Length
Volume = 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

measured's People

Contributors

alexcrocha avatar anthonybobsin avatar arturopie avatar bitwise-aiden avatar byroot avatar casperisfine avatar cyprusad avatar disaacs avatar dvisockas avatar fekadeabdejene avatar garethson avatar javierhonduco avatar jmortlock avatar jules2689 avatar keiththomps avatar kirinrastogi avatar kmcphillips avatar kushagra-03 avatar lucasuyezu avatar martinbjeldbak avatar paracycle avatar rosner avatar saraid avatar shopify-admins avatar thegedge avatar trishume 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  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  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  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

measured's Issues

Make Conversion immutable

I see little to no value in having the ability to add units to Measured::Conversion after it's been created. Immutability again will give us some nice benefits here, in particular with how I'd approach #32 (increased performance).

I think a DSL or configuration step would be really nice here, which is basically how we use it today. I'll throw out a couple of options:

Close to what we have today

class Measured::Length < Measured::Measurable
  conversion.configure do
    case_sensitive true

    base :m, aliases: [:meter, :metre, :meters, :metres]

    unit :cm, value: "0.01 m", aliases: [:centimeter, :centimetre, :centimeters, :centimetres],
  end
end

DSL

Measured::Length = Measured::Measurable.build("Length") do
  case_sensitive false

  base :m, aliases: [:meter, :metre, :meters, :metres]

  unit :cm, value: "0.01   m", aliases: [:centimeter, :centimetre, :centimeters, :centimetres]
  unit :mm, value: "0.001  m", aliases: [:millimeter, :millimetre, :millimeters, :millimetres]
  unit :in, value: "0.0254 m", aliases: [:inch, :inches]
  unit :ft, value: "0.3048 m", aliases: [:foot, :feet]
  unit :yd, value: "0.9144 m", aliases: [:yard, :yards]
end

Support multiplication of units

We previously supported multiplication between Measurables, but specifically the inner product:

(Measured::Length(2, :cm) * Measured::Length(3, :cm)).to_s
> 6 cm

This is convenient, but not (mathematically) correct. When multiplying two measured values we should combine units from the same system, raising the exponent, and then multiplying/dividing the other units:

Measured::Length(2, :cm) * Measured::Length(3, :cm)
> 6 cm^2

Measured::Length(2, :m) * Measured::Length(0.002, :km) / Measured::Time(0.1, :seconds)
> 40 m/s

This change should not come at a performance penalty for any current use cases:

  • unit + unit
  • unit.scale(scalar)
  • unit.convert_to(:simple_unit)
  • etc

Use Rational instead of BigDecimal?

There's no loss in operating with either, but there are pros and cons we need to weight against each other:

Representation

Rational has the benefit of describing infinitely repeating rationals exactly (e.g., 1/3, 4/7), but cannot represent infinity.

String parsing

Rational and BigDecimal have different profiles based on the type of string being parsed. Rational is a little faster for integral values, but much slower for fractional values.

Integral

Benchmark.ips do |x|
  FOO = "10127398"
  x.report("to_r") { FOO.to_r }
  x.report("to_d") { FOO.to_d }
  x.compare!
end
Warming up --------------------------------------
                to_r   117.596k i/100ms
                to_d   100.975k i/100ms
Calculating -------------------------------------
                to_r      1.913M (± 5.4%) i/s -      9.643M in   5.058549s
                to_d      1.488M (± 5.4%) i/s -      7.472M in   5.038070s

Comparison:
                to_r:  1912752.0 i/s
                to_d:  1488150.4 i/s - 1.29x  slower

Non-integral

Benchmark.ips do |x|
  FOO = "10.127398"
  x.report("to_r") { FOO.to_r }
  x.report("to_d") { FOO.to_d }
  x.compare!
end
Warming up --------------------------------------
                to_r    58.234k i/100ms
                to_d    97.379k i/100ms
Calculating -------------------------------------
                to_r    715.125k (± 4.3%) i/s -      3.611M in   5.058701s
                to_d      1.447M (± 3.8%) i/s -      7.303M in   5.056448s

Comparison:
                to_d:  1446574.0 i/s
                to_r:   715125.3 i/s - 2.02x  slower

Arithmetic

Rational has much better performance for arithmetic.

Benchmark.ips do |x|
  VALUE = "123.512"  
  R = VALUE.to_r
  BD = VALUE.to_d

  x.report("rational") { R + 10*R }
  x.report("bigdecimal") { BD + 10*BD }
  x.compare!
end
Warming up --------------------------------------
            rational    84.517k i/100ms
          bigdecimal    40.148k i/100ms
Calculating -------------------------------------
            rational      1.166M (± 4.1%) i/s -      5.832M in   5.008772s
          bigdecimal    506.024k (± 3.5%) i/s -      2.529M in   5.004639s

Comparison:
            rational:  1166439.0 i/s
          bigdecimal:   506023.8 i/s - 2.31x  slower

Defect: Unexpected behaviour when building with conflicting conversions

I'm creating this issue to check if the maintainers also consider this behaviour a defect. If they do, I can work on a PR to fix it.

Example 1

Steps to Reproduce

System = Measured.build do
  unit "cases", value: "1 pallets"
  unit "pallets", value: "99 cases"
end

Expected Behaviour

Raise exception from Measured.build, as we are providing conflicting conversions.

Actual Behaviour

No exception, and:

System.new(1, "cases").convert_to("pallets").to_s
 => "1 pallets"
System.new(1, "pallets").convert_to("cases").to_s
 => "1 cases"

Example 2

System = Measured.build do
  unit "pallets", value: "1 liters"
  unit "liters", value: "0.1 cases"
  unit "cases", value: "0.1 pallets"
end

Expected Behaviour

Raise exception from Measured.build, as we are providing conflicting conversions.

Actual Behaviour

No exception, and:

System.new(1, "liters")
  .convert_to("cases")
  .convert_to("pallets")
  .convert_to("liters")
  .to_s
 => "0.01 liters"

Forwardable is Never required

extend Forwardable

/tmp >gem list measured

*** LOCAL GEMS ***

measured (2.3.0)
/tmp >irb
irb [2.4.0] (tmp)$ require "measured"
NameError: uninitialized constant #<Class:Measured::Measurable>::Forwardable
	from /Users/sshaw/.rvm/gems/ruby-2.4.0/gems/measured-2.3.0/lib/measured/measurable.rb:65:in `singleton class'
	from /Users/sshaw/.rvm/gems/ruby-2.4.0/gems/measured-2.3.0/lib/measured/measurable.rb:64:in `<class:Measurable>'
	from /Users/sshaw/.rvm/gems/ruby-2.4.0/gems/measured-2.3.0/lib/measured/measurable.rb:1:in `<top (required)>'
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:68:in `require'
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:68:in `require'
	from /Users/sshaw/.rvm/gems/ruby-2.4.0/gems/measured-2.3.0/lib/measured/base.rb:46:in `<top (required)>'
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:68:in `require'
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:68:in `require'
	from /Users/sshaw/.rvm/gems/ruby-2.4.0/gems/measured-2.3.0/lib/measured.rb:1:in `<top (required)>'
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:133:in `require'
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:133:in `rescue in require'
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:40:in `require'
	from (irb):1
	from /Users/sshaw/.rvm/rubies/ruby-2.4.0/bin/irb:11:in `<main>'
irb [2.4.0] (tmp)$ require "forwardable"
=> true
irb [2.4.0] (tmp)$ require "measured"
=> true

Make adding units with SI prefixes simpler

I'm thinking we could add an SI helper when building a unit system, which will generate units with the SI prefixes added. Something along the lines:

si_unit :g, aliases: [:gram, :grams]
unit :g, aliases: [:gram, :grams], with_si_prefixes: true

This would generate kilograms, megagrams, micrograms, etc.

Don't cast values to `BigDecimal`

Allow clients to decide what type of values a Measurable has. This allows clients to not incur the cost of a BigDecimal if they don't need it. When a non-integral calculation has to be made, we can coerce things to BigDecimal.

Convert `ConversionTable` to module functions

ConversionTable makes no sense, but rather encapsulates a set of operations to create a Hash for unit conversion. Since it's not meant to stick around we should just stick to exposing a function.

Use Numeric for Measurable

Numeric supplies an interface for classes that act like numbers, and a Measurable fits this model. Coercion of [a, b] would require converting b to the same units of a.

This will also allow us to drop the Comparable portion of Measurable since Numeric gives us that for free.

Unexpected formatting of formatted output for converted weight

Hello,
I'm running into a bit of an unexpected behaviour with the following code:

weight = Measured::Weight.new(value, 'kg')
weight = weight.convert_to('lbs') if current_user&.prefers_imperial?
return weight.format('%.2<value>f %<unit>s')

Now I'd expect to just see this either in KG or LBS based on the user's preferences, but instead when the weight is converted, format gives me the full conversion string for the unit, eg. 319.67 lb (45359237/100000000 kg)
How do I get around this?

Implement `Measured::Temperature`

Our unit system conversion assumes all units share a 0 and there is a multiplicative factor between them.

This falls apart for temperature.

To assure we are making valid assumptions and have a relatively generic conversion table system, we should implement temperature conversion. Between F/C/K.

We can not include it by default. But put it into the README and show how to include it.

We can use ActiveSupport::Testing::Isolation to test it if needed without requiring it globally: http://api.rubyonrails.org/classes/ActiveSupport/Testing/Isolation.html

Documentation Example for Persisting Units

Fantastic library that you have created.

I am curious about the best-practices for validating and persisting these into databases.

Do you write new validators each time you support weight measurements?

Add an alias to an existing unit

Is there a way to add an extra alias to an existing unit? I wanted to add gm alias to Measured::Weight as an alias for grams. But don't see a straightforward way of accomplishing that.

Looks like I might have to skip all the units and then redefine them using Measured.build, which seems like quite some work to add an alias.

#146 (comment)

First Conversion Very Slow

This seems to be the case with Weight and other conversions too but fl oz to ml is the slowest so far:

irb [2.4.0] (tmp)$ v = Measured::Volume.parse("123 fl_oz")
=> #<Measured::Volume: 123 #<Measured::Unit: oz (fl_oz, imp_fl_oz, imperial_fluid_ounce, imperial_fluid_ounces) 1/160 gal>>
irb [2.4.0] (tmp)$ Benchmark.measure { v.convert_to("ml") }.real
=> 2.460711195017211

Remove type switch from Measurable#initialize

The type switching in Measurable#initialize means that constructing a Measurable will always incur unnecessary cost. Clients generally know what they have, so have factory functions on the class (e.g., from_decimal) that clients use to construct Measurables

Better serialization and deserialization

👋 Hi!

If we initialize a weight value and serialize it:

weight = Measured::Weight.new(0, :g)
serialized_weight = { value: weight.value, unit: weight.unit.name }.to_json
#=> '{"value":"0\/1","unit":"g"}'

If we then deserialize it:

deserialized_weight = JSON.parse(serialized_weight)
weight = Measured::Weight.new(deserialized_weight['value'], deserialized_weight['unit'])
#=> ArgumentError: invalid value for BigDecimal(): "0/1"

This is the case because Measured assumes that if the first argument is a string - it is parsed as a BigDecimal.
Maybe it could be a bit smarter and try to parse the value as Rational if some condition (maybe just a simple \d+\\\d+ regex matched)?

Rounding and measured

👋

@alexgomez54 and I have bumped into the use case of representing a value and its unit as a string (like in Measured#to_s's spirit`) but specifying some rounding.

Usually, our code looks like:

def weight_with_decimals(decimals = 2)
  rounded_value = weight.value.round(decimals)  # weight.is_a?(Measured::Weight) => true
  Measured::Weight.new(rounded_value, weight.unit).to_s
end

We were wondering if it would make sense to have a feature that does something like that, in a more generic way, inside of Measured 😄 .

What do you think @thegedge @kmcphillips @benwah? 😀

Remove add_alias

Will make Measured::Unit immutable, which makes it far easier to do various performance improvements around name to unit mappings. Also, always a nice property to have!

`convert_to!` shouldn't do a heap allocation

Measurable#convert_to! calls Measurable#convert_to to perform its operation. In-place operations are often used for performance reasons, and should not cause (unnecessary) allocations. convert_to! should do so, and convert_to should be written as self.dup.convert_to!(unit)

That being said the better option here is to just drop convert_to! in favour of immutability.

Simplify case sensitivity code

Use something other than case_sensitive as state for Conversion. Some ideas include:

  1. Use dependency injection to supply a collection object that will store names/aliases. If case insensitivity is required, this collection should be able to hide that detail.
  2. Have a "name mapping" function in Conversion that can be overridden by another class (e.g., CaseSensitiveConversion would be the identity function and CaseInsensitiveConversion would be #downcase)

Custom unit alias regression in 2.5.1

We recently upgraded out gems and moved from measured 2.4 to 2.5.1 and noticed that some of our custom units and aliases stopped working.

Here is a fabricated example:

In measured.rb initializer

Measured::Cardinality = Measured.build do
  unit :piece, aliases: [:pieces]
  unit :dozen, aliases: [:dz], value: "12 pieces"
end

Key issue:

  • We reference a alias pieces in the value definition for dozen and that seems to have broken

Previous/Expected behavior:

  • Loads without issue

Observed behavior:

Measured::UnitError: Cannot find conversion path from dozen to piece.
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:43:in `find_conversion'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:31:in `block (2 levels) in generate_table'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:30:in `each'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:30:in `block in generate_table'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:27:in `each'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:27:in `each_with_object'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:27:in `generate_table'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/conversion_table_builder.rb:13:in `to_h'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/unit_system.rb:13:in `initialize'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/unit_system_builder.rb:24:in `new'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/unit_system_builder.rb:24:in `build'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/base.rb:21:in `block in build'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/base.rb:16:in `initialize'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/base.rb:16:in `new'
/Users/trcarden/.rvm/gems/ruby-2.6.5@titan/gems/measured-2.5.1/lib/measured/base.rb:16:in `build'
***/measured.rb:6:in `<main>'```

Fix skipped tests for precision loss

Look at the best way to deal with precision loss with rounding. Possibly pass Rational all the way through if any factor is a rational. Possibly force rounding. Possibly test rounded values to the meaningful number of significant digits.

Case sensitivity

For example:

unit?("KG")
Measured::UnitError: Cannot find unit for KG.

At the very least this should return false and not raise.

Do we need `base_unit`?

Discussion: does Measured::Conversion.base_unit add any value, or can we remove it to simplify the interface?

Is there anyway to dynamically add a unit?

I'm trying to figure out how I can dynamically add units to Measured::Weight. I'm working on an app for farmers and I need them to be able to add weights for things like bunches or container. The only problem is every farmer could set them differently.

Do not allow arithmetic with non-Measured types

Without a unit, it's really hard to understand intent and could lead to subtle bugs / surprises. For example, what does Measured::Length.new(1, :cm) + 3 mean? Was the intent to add 3cm? This is a case where explicitness is a must.

Although we could argue that identity elements – 1 for multiplication / division, 0 for addition and subtraction (when on the RHS) – would often make sense, they don't always. For example, 0°C + 10°F is 42°F, not 10°F.

Remove base unit

We define base unit without a value, then a unit value: 'other unit'.

Explore if we can remove the base altogether. Building the parse graph may be more complex, but we can experiment with just processing them in order and not depending on a base.

This may help us with #59 that don't share a common zero point, and simply express units relative to other units.

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.