measured 2.5.0 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +32 -0
  3. data/CHANGELOG.md +40 -0
  4. data/README.md +3 -2
  5. data/gemfiles/{activesupport-5.0.gemfile → activesupport-5.2.gemfile} +1 -1
  6. data/gemfiles/activesupport-6.0.gemfile +1 -1
  7. data/gemfiles/{activesupport-5.1.gemfile → activesupport-6.1.gemfile} +1 -1
  8. data/lib/measured.rb +1 -0
  9. data/lib/measured/arithmetic.rb +1 -0
  10. data/lib/measured/base.rb +7 -5
  11. data/lib/measured/cache/json.rb +12 -5
  12. data/lib/measured/cache/json_writer.rb +1 -0
  13. data/lib/measured/cache/null.rb +1 -0
  14. data/lib/measured/conversion_table_builder.rb +21 -2
  15. data/lib/measured/cycle_detected.rb +11 -0
  16. data/lib/measured/measurable.rb +18 -21
  17. data/lib/measured/missing_conversion_path.rb +12 -0
  18. data/lib/measured/parser.rb +2 -1
  19. data/lib/measured/unit.rb +16 -26
  20. data/lib/measured/unit_already_added.rb +11 -0
  21. data/lib/measured/unit_error.rb +4 -0
  22. data/lib/measured/unit_system.rb +16 -20
  23. data/lib/measured/unit_system_builder.rb +4 -3
  24. data/lib/measured/units/length.rb +1 -0
  25. data/lib/measured/units/volume.rb +1 -0
  26. data/lib/measured/units/weight.rb +1 -0
  27. data/lib/measured/version.rb +2 -1
  28. data/measured.gemspec +10 -2
  29. data/test/arithmetic_test.rb +1 -0
  30. data/test/cache/json_test.rb +1 -0
  31. data/test/cache/json_writer_test.rb +1 -0
  32. data/test/cache/null_test.rb +1 -0
  33. data/test/cache_consistency_test.rb +1 -0
  34. data/test/conversion_table_builder_test.rb +11 -0
  35. data/test/measurable_test.rb +1 -1
  36. data/test/parser_test.rb +1 -0
  37. data/test/support/always_true_cache.rb +1 -0
  38. data/test/support/fake_system.rb +1 -0
  39. data/test/support/subclasses.rb +1 -0
  40. data/test/test_helper.rb +2 -1
  41. data/test/unit_error_test.rb +1 -0
  42. data/test/unit_system_builder_test.rb +28 -2
  43. data/test/unit_system_test.rb +1 -0
  44. data/test/unit_test.rb +1 -0
  45. data/test/units/length_test.rb +5 -4
  46. data/test/units/volume_test.rb +6 -5
  47. data/test/units/weight_test.rb +1 -0
  48. metadata +19 -14
  49. data/.travis.yml +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cc64e96e1ca065fbd9bdbe43bc80d9ae99fb2ffc52ddcba131180e439bfa462
4
- data.tar.gz: d7bd0237fb1dfb6f9c38a86ae32f0cc0d499b547aca8a7b2ddcc531ba7daabd8
3
+ metadata.gz: 2641c37b73279794a9877f2c22d4fc40ed49e3fb66ec879863d631251befec60
4
+ data.tar.gz: d065ce4bd8cc5dd3b75844aa08a2bbd931ca83bf214ea3eb5fb7db1e67c08a23
5
5
  SHA512:
6
- metadata.gz: c017c759124694968bb8c283c5f6a5f33af3f25544d9fe9fa190dcaeac59ce7e0a708019158604219fa3b332f9f2f022fe44bfbfa963682a65e50f63f583f5ec
7
- data.tar.gz: 6096418c372b51a8229148618a268711956e3bc3bcbbef0e07597a60b5ef52d0ed1fc314f433ee94968231fb633efad657d281258f12313220226824320875e5
6
+ metadata.gz: 8b7b34fd57b3bff6c467ba62156d21a89fadcd32e009cecb3c1ca36b609134ed4abd1950ac7513ba5ced802627a723086ba44ee563286e1294759dee1a0eeeb1
7
+ data.tar.gz: 9e2d5a86949c31b44c810c186d46b080bbc599d3f87a276676eb249b6b0841b41dc7964b4c071c98c21be3ab071f8db0085d9cf50d9134b1e377e62b545aae41
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ env:
9
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
10
+ strategy:
11
+ matrix:
12
+ ruby:
13
+ - '2.5'
14
+ - '2.6'
15
+ - '2.7'
16
+ - '3.0'
17
+ gemfile:
18
+ - Gemfile
19
+ - gemfiles/activesupport-5.2.gemfile
20
+ - gemfiles/activesupport-6.0.gemfile
21
+ - gemfiles/activesupport-6.1.gemfile
22
+ name: Ruby ${{ matrix.ruby }} ${{ matrix.gemfile }}
23
+ steps:
24
+ - uses: actions/checkout@v1
25
+ - name: Set up Ruby ${{ matrix.ruby }}
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ bundler-cache: true
30
+ - name: Run tests
31
+ run: |
32
+ bundle exec rake
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ Unreleased
2
+ -----
3
+
4
+ 2.7.1
5
+ -----
6
+
7
+ * Fix Ruby 3.0 compatibility
8
+
9
+ 2.7.0
10
+ -----
11
+
12
+ * Raises an exception on cyclic conversions. (@arturopie)
13
+ * Deduplicate strings loaded from the cache.
14
+ * Deduplicate parsed units.
15
+
16
+ 2.6.0
17
+ -----
18
+
19
+ * Add `Measured::MissingConversionPath` and `Measured::UnitAlreadyAdded` as subclasses of `Measured::UnitError` to handle specific error cases. (@arturopie)
20
+ * Support only ActiveSupport 5.2 and above.
21
+
22
+
23
+ 2.5.2
24
+ -----
25
+
26
+ * Allow unit values to be declared in the unit system through aliases and not just the base unit name.
27
+ * Fix some deprecations in tests and CI.
28
+
29
+ 2.5.1
30
+ ----
31
+
32
+ * Get rid of most memoizations in favor of eager computations.
33
+
34
+ 2.5.0
35
+ -----
36
+
37
+ * Add `CHANGELOG.md`.
38
+ * Fix some deprecations and warnings.
39
+ * Support Rails 6 and Ruby 2.6.
40
+ * Cache conversion table in JSON file for first load performance.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Measured [![Build Status](https://travis-ci.org/Shopify/measured.svg)](https://travis-ci.org/Shopify/measured) [![Gem Version](https://badge.fury.io/rb/measured.svg)](http://badge.fury.io/rb/measured)
1
+ # Measured [![Build Status](https://github.com/Shopify/measured/workflows/CI/badge.svg)](https://github.com/Shopify/measured/actions?query=workflow%3ACI)
2
2
 
3
3
  Encapsulates measurements with their units. Provides easy conversion between units. Built in support for weight, length, and volume.
4
4
 
@@ -273,12 +273,13 @@ Existing alternatives which were considered:
273
273
 
274
274
  ### Gem: [unitwise](https://github.com/joshwlewis/unitwise)
275
275
  * **Pros**
276
- * Well written and maintained.
276
+ * Well written.
277
277
  * Conversions done with Unified Code for Units of Measure (UCUM) so highly accurate and reliable.
278
278
  * **Cons**
279
279
  * Lots of code. Good code, but lots of it.
280
280
  * Many modifications to core types.
281
281
  * ActiveRecord adapter exists but is written and maintained by a different person/org.
282
+ * Not actively maintained.
282
283
 
283
284
  ## Contributing
284
285
 
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec path: '..'
4
4
 
5
- gem 'activesupport', '~> 5.0'
5
+ gem 'activesupport', '~> 5.2'
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec path: '..'
4
4
 
5
- gem 'activesupport', '~> 6.0.0.rc1'
5
+ gem 'activesupport', '~> 6.0'
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec path: '..'
4
4
 
5
- gem 'activesupport', '~> 5.1'
5
+ gem 'activesupport', '~> 6.1'
data/lib/measured.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "measured/base"
2
3
 
3
4
  require "measured/units/length"
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured::Arithmetic
2
3
  def +(other)
3
4
  arithmetic_operation(other, :+)
data/lib/measured/base.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "forwardable"
2
3
  require "measured/version"
3
4
  require "active_support/all"
@@ -5,8 +6,6 @@ require "bigdecimal"
5
6
  require "json"
6
7
 
7
8
  module Measured
8
- class UnitError < StandardError ; end
9
-
10
9
  class << self
11
10
  def build(&block)
12
11
  builder = UnitSystemBuilder.new
@@ -23,10 +22,9 @@ module Measured
23
22
 
24
23
  def method_missing(method, *args)
25
24
  class_name = "Measured::#{ method }"
25
+ klass = class_name.safe_constantize
26
26
 
27
- if Measurable.subclasses.map(&:to_s).include?(class_name)
28
- klass = class_name.constantize
29
-
27
+ if klass && klass < Measurable
30
28
  Measured.define_singleton_method(method) do |value, unit|
31
29
  klass.new(value, unit)
32
30
  end
@@ -39,6 +37,10 @@ module Measured
39
37
  end
40
38
  end
41
39
 
40
+ require "measured/unit_error"
41
+ require "measured/cycle_detected"
42
+ require "measured/unit_already_added"
43
+ require "measured/missing_conversion_path"
42
44
  require "measured/arithmetic"
43
45
  require "measured/parser"
44
46
  require "measured/unit"
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured::Cache
2
3
  class Json
3
4
  attr_reader :filename, :path
@@ -13,7 +14,7 @@ module Measured::Cache
13
14
 
14
15
  def read
15
16
  return unless exist?
16
- decode(JSON.load(File.read(@path)))
17
+ decode(JSON.load(File.read(@path), nil, freeze: true))
17
18
  end
18
19
 
19
20
  def write(table)
@@ -36,11 +37,17 @@ module Measured::Cache
36
37
  end
37
38
 
38
39
  def decode(table)
39
- table.each_with_object(table.dup) do |(k1, v1), accu|
40
- v1.each do |k2, v2|
41
- if v2.is_a?(Hash)
42
- accu[k1][k2] = Rational(v2["numerator"], v2["denominator"])
40
+ table.transform_values do |value1|
41
+ if value1.is_a?(Hash)
42
+ value1.transform_values do |value2|
43
+ if value2.is_a?(Hash)
44
+ Rational(value2["numerator"], value2["denominator"])
45
+ else
46
+ value2
47
+ end
43
48
  end
49
+ else
50
+ value1
44
51
  end
45
52
  end
46
53
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured::Cache::JsonWriter
2
3
  def write(table)
3
4
  File.open(@path, "w") do |f|
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured::Cache
2
3
  class Null
3
4
  def exist?
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::ConversionTableBuilder
2
3
  attr_reader :units
3
4
 
@@ -23,6 +24,8 @@ class Measured::ConversionTableBuilder
23
24
  private
24
25
 
25
26
  def generate_table
27
+ validate_no_cycles
28
+
26
29
  units.map(&:name).each_with_object({}) do |to_unit, table|
27
30
  to_table = {to_unit => Rational(1, 1)}
28
31
 
@@ -36,10 +39,27 @@ class Measured::ConversionTableBuilder
36
39
  end
37
40
  end
38
41
 
42
+ def validate_no_cycles
43
+ graph = units.select { |unit| unit.conversion_unit.present? }.group_by { |unit| unit.name }
44
+ validate_acyclic_graph(graph, from: graph.keys[0])
45
+ end
46
+
47
+ # This uses a depth-first search algorithm: https://en.wikipedia.org/wiki/Depth-first_search
48
+ def validate_acyclic_graph(graph, from:, visited: [])
49
+ graph[from]&.each do |edge|
50
+ adjacent_node = edge.conversion_unit
51
+ if visited.include?(adjacent_node)
52
+ raise Measured::CycleDetected.new(edge)
53
+ else
54
+ validate_acyclic_graph(graph, from: adjacent_node, visited: visited + [adjacent_node])
55
+ end
56
+ end
57
+ end
58
+
39
59
  def find_conversion(to:, from:)
40
60
  conversion = find_direct_conversion_cached(to: to, from: from) || find_tree_traversal_conversion(to: to, from: from)
41
61
 
42
- raise Measured::UnitError, "Cannot find conversion path from #{ from } to #{ to }." unless conversion
62
+ raise Measured::MissingConversionPath.new(from, to) unless conversion
43
63
 
44
64
  conversion
45
65
  end
@@ -87,5 +107,4 @@ class Measured::ConversionTableBuilder
87
107
 
88
108
  nil
89
109
  end
90
-
91
110
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class CycleDetected < UnitError
4
+ attr_reader :unit
5
+
6
+ def initialize(unit)
7
+ super("The following conversion introduces cycles in the unit system: #{unit}. Remove the conversion or fix the cycle.")
8
+ @unit = unit
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::Measurable < Numeric
2
- DEFAULT_FORMAT_STRING = '%.2<value>f %<unit>s'
3
+ DEFAULT_FORMAT_STRING = "%.2<value>f %<unit>s"
3
4
 
4
5
  include Measured::Arithmetic
5
6
 
@@ -19,6 +20,18 @@ class Measured::Measurable < Numeric
19
20
  else
20
21
  BigDecimal(value)
21
22
  end
23
+
24
+ @value_string = begin
25
+ str = case value
26
+ when Rational
27
+ value.denominator == 1 ? value.numerator.to_s : value.to_f.to_s
28
+ when BigDecimal
29
+ value.to_s("F")
30
+ else
31
+ value.to_f.to_s
32
+ end
33
+ str.gsub(/\.0*\Z/, "")
34
+ end.freeze
22
35
  end
23
36
 
24
37
  def convert_to(new_unit)
@@ -39,18 +52,16 @@ class Measured::Measurable < Numeric
39
52
  end
40
53
 
41
54
  def to_s
42
- @to_s ||= "#{value_string} #{unit.name}"
55
+ "#{@value_string} #{unit.name}"
43
56
  end
44
57
 
45
58
  def humanize
46
- @humanize ||= begin
47
- unit_string = value == 1 ? unit.name : ActiveSupport::Inflector.pluralize(unit.name)
48
- "#{value_string} #{unit_string}"
49
- end
59
+ unit_string = value == 1 ? unit.name : ActiveSupport::Inflector.pluralize(unit.name)
60
+ "#{@value_string} #{unit_string}"
50
61
  end
51
62
 
52
63
  def inspect
53
- @inspect ||= "#<#{self.class}: #{value_string} #{unit.inspect}>"
64
+ "#<#{self.class}: #{@value_string} #{unit.inspect}>"
54
65
  end
55
66
 
56
67
  def <=>(other)
@@ -86,18 +97,4 @@ class Measured::Measurable < Numeric
86
97
  def unit_from_unit_or_name!(value)
87
98
  value.is_a?(Measured::Unit) ? value : self.class.unit_system.unit_for!(value)
88
99
  end
89
-
90
- def value_string
91
- @value_string ||= begin
92
- str = case value
93
- when Rational
94
- value.denominator == 1 ? value.numerator.to_s : value.to_f.to_s
95
- when BigDecimal
96
- value.to_s("F")
97
- else
98
- value.to_f.to_s
99
- end
100
- str.gsub(/\.0*\Z/, "")
101
- end
102
- end
103
100
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class MissingConversionPath < UnitError
4
+ attr_reader :from, :to
5
+
6
+ def initialize(from, to)
7
+ super("Cannot find conversion path from #{from} to #{to}.")
8
+ @from = from
9
+ @to = to
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured::Parser
2
3
  extend self
3
4
 
@@ -32,6 +33,6 @@ module Measured::Parser
32
33
 
33
34
  raise Measured::UnitError, "Cannot parse measurement from '#{string}'" unless result
34
35
 
35
- [result.captures[0].to_r, result.captures[1]]
36
+ [result.captures[0].to_r, -result.captures[1]]
36
37
  end
37
38
  end
data/lib/measured/unit.rb CHANGED
@@ -1,43 +1,41 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::Unit
2
3
  include Comparable
3
4
 
4
- attr_reader :name, :aliases, :conversion_amount, :conversion_unit, :unit_system
5
+ attr_reader :name, :names, :aliases, :conversion_amount, :conversion_unit, :unit_system, :inverse_conversion_amount
5
6
 
6
7
  def initialize(name, aliases: [], value: nil, unit_system: nil)
7
8
  @name = name.to_s.freeze
8
9
  @aliases = aliases.map(&:to_s).map(&:freeze).freeze
10
+ @names = ([@name] + @aliases).sort!.freeze
9
11
  @conversion_amount, @conversion_unit = parse_value(value) if value
12
+ @inverse_conversion_amount = (1 / conversion_amount if conversion_amount)
13
+ @conversion_string = ("#{conversion_amount} #{conversion_unit}" if conversion_amount || conversion_unit)
10
14
  @unit_system = unit_system
11
15
  end
12
16
 
13
- def with_unit_system(unit_system)
17
+ def with(name: nil, unit_system: nil, aliases: nil, value: nil)
14
18
  self.class.new(
15
- name,
16
- aliases: aliases,
17
- value: conversion_string,
18
- unit_system: unit_system
19
+ name || self.name,
20
+ aliases: aliases || self.aliases,
21
+ value: value || @conversion_string,
22
+ unit_system: unit_system || self.unit_system
19
23
  )
20
24
  end
21
25
 
22
- def names
23
- @names ||= ([name] + aliases).sort!.freeze
24
- end
25
-
26
26
  def to_s
27
- @to_s ||= if conversion_string
28
- "#{name} (#{conversion_string})".freeze
27
+ if @conversion_string
28
+ "#{name} (#{@conversion_string})".freeze
29
29
  else
30
30
  name
31
31
  end
32
32
  end
33
33
 
34
34
  def inspect
35
- @inspect ||= begin
36
- pieces = [name]
37
- pieces << "(#{aliases.join(", ")})" if aliases.any?
38
- pieces << conversion_string if conversion_string
39
- "#<#{self.class.name}: #{pieces.join(" ")}>".freeze
40
- end
35
+ pieces = [name]
36
+ pieces << "(#{aliases.join(", ")})" if aliases.any?
37
+ pieces << @conversion_string if @conversion_string
38
+ "#<#{self.class.name}: #{pieces.join(" ")}>".freeze
41
39
  end
42
40
 
43
41
  def <=>(other)
@@ -53,16 +51,8 @@ class Measured::Unit
53
51
  end
54
52
  end
55
53
 
56
- def inverse_conversion_amount
57
- @inverse_conversion_amount ||= 1 / conversion_amount if conversion_amount
58
- end
59
-
60
54
  private
61
55
 
62
- def conversion_string
63
- @conversion_string ||= ("#{conversion_amount} #{conversion_unit}" if conversion_amount || conversion_unit)
64
- end
65
-
66
56
  def parse_value(tokens)
67
57
  case tokens
68
58
  when String
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class UnitAlreadyAdded < UnitError
4
+ attr_reader :unit_name
5
+
6
+ def initialize(unit_name)
7
+ super("Unit #{unit_name} has already been added.")
8
+ @unit_name = unit_name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class UnitError < StandardError ; end
4
+ end
@@ -1,17 +1,22 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::UnitSystem
2
- attr_reader :units
3
+ attr_reader :units, :unit_names, :unit_names_with_aliases
3
4
 
4
5
  def initialize(units, cache: nil)
5
- @units = units.map { |unit| unit.with_unit_system(self) }
6
+ @units = units.map { |unit| unit.with(unit_system: self) }
7
+ @units = @units.map do |unit|
8
+ next unit unless unit.conversion_unit
9
+ conversion_unit = @units.find { |u| u.names.include?(unit.conversion_unit) }
10
+ next unit unless conversion_unit
11
+ unit.with(value: [unit.conversion_amount, conversion_unit.name])
12
+ end
13
+ @unit_names = @units.map(&:name).sort.freeze
14
+ @unit_names_with_aliases = @units.flat_map(&:names).sort.freeze
15
+ @unit_name_to_unit = @units.each_with_object({}) do |unit, hash|
16
+ unit.names.each { |name| hash[name.to_s] = unit }
17
+ end
6
18
  @conversion_table_builder = Measured::ConversionTableBuilder.new(@units, cache: cache)
7
- end
8
-
9
- def unit_names_with_aliases
10
- @unit_names_with_aliases ||= @units.flat_map(&:names).sort
11
- end
12
-
13
- def unit_names
14
- @unit_names ||= @units.map(&:name).sort
19
+ @conversion_table = @conversion_table_builder.to_h.freeze
15
20
  end
16
21
 
17
22
  def unit_or_alias?(name)
@@ -51,14 +56,5 @@ class Measured::UnitSystem
51
56
 
52
57
  protected
53
58
 
54
- def conversion_table
55
- @conversion_table ||= @conversion_table_builder.to_h
56
- end
57
-
58
- def unit_name_to_unit
59
- @unit_name_to_unit ||= @units.inject({}) do |hash, unit|
60
- unit.names.each { |name| hash[name.to_s] = unit }
61
- hash
62
- end
63
- end
59
+ attr_reader :unit_name_to_unit, :conversion_table
64
60
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::UnitSystemBuilder
2
3
  def initialize
3
4
  @units = []
@@ -45,8 +46,8 @@ class Measured::UnitSystemBuilder
45
46
  ["P", "peta", 15],
46
47
  ["E", "exa", 18],
47
48
  ["Z", "zetta", 21],
48
- ["Y", "yotta", 24]
49
- ]
49
+ ["Y", "yotta", 24],
50
+ ].map(&:freeze).freeze
50
51
 
51
52
  def build_si_units(name, aliases: [], value: nil)
52
53
  si_units = [build_unit(name, aliases: aliases, value: value)]
@@ -66,7 +67,7 @@ class Measured::UnitSystemBuilder
66
67
  def check_for_duplicate_unit_names!(unit)
67
68
  names = @units.flat_map(&:names)
68
69
  if names.any? { |name| unit.names.include?(name) }
69
- raise Measured::UnitError, "Unit #{unit.name} has already been added."
70
+ raise Measured::UnitAlreadyAdded.new(unit.name)
70
71
  end
71
72
  end
72
73
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  Measured::Length = Measured.build do
2
3
  si_unit :m, aliases: [:meter, :metre, :meters, :metres]
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  Measured::Volume = Measured.build do
2
3
  si_unit :l, aliases: [:liter, :litre, :liters, :litres]
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  Measured::Weight = Measured.build do
2
3
  si_unit :g, aliases: [:gram, :grams]
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured
2
- VERSION = "2.5.0"
3
+ VERSION = "2.7.1"
3
4
  end
data/measured.gemspec CHANGED
@@ -13,16 +13,24 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/Shopify/measured"
14
14
  spec.license = "MIT"
15
15
 
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ else
21
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ end
23
+
16
24
  spec.files = `git ls-files -z`.split("\x0")
17
25
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
26
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
27
  spec.require_paths = ["lib"]
20
28
 
21
- spec.add_runtime_dependency "activesupport", ">= 5.0"
29
+ spec.add_runtime_dependency "activesupport", ">= 5.2"
22
30
 
23
31
  spec.add_development_dependency "rake", "> 10.0"
24
32
  spec.add_development_dependency "minitest", "> 5.5.1"
25
33
  spec.add_development_dependency "minitest-reporters"
26
- spec.add_development_dependency "mocha", "> 1.1.0"
34
+ spec.add_development_dependency "mocha", ">= 1.4.0"
27
35
  spec.add_development_dependency "pry"
28
36
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::ArithmeticTest < ActiveSupport::TestCase
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::Cache::JsonTest < ActiveSupport::TestCase
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::Cache::JsonWriterTest < ActiveSupport::TestCase
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::Cache::NullTest < ActiveSupport::TestCase
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  # In general we do not want to expose the inner workings of the caching and unit systems as part of the API.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
@@ -116,6 +117,16 @@ class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
116
117
  end
117
118
  end
118
119
 
120
+ test "#to_h raises exception when there are cycles" do
121
+ unit1 = Measured::Unit.new(:pallets, value: "1 liters")
122
+ unit2 = Measured::Unit.new(:liters, value: "0.1 cases")
123
+ unit3 = Measured::Unit.new(:cases, value: "0.1 pallets")
124
+
125
+ assert_raises(Measured::CycleDetected) do
126
+ Measured::ConversionTableBuilder.new([unit1, unit2, unit3]).to_h
127
+ end
128
+ end
129
+
119
130
  test "#cached? returns true if there's a cache" do
120
131
  builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
121
132
  assert_predicate builder, :cached?
@@ -1,7 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::MeasurableTest < ActiveSupport::TestCase
4
-
5
5
  setup do
6
6
  @arcane = Magic.unit_system.unit_for!(:arcane)
7
7
  @fireball = Magic.unit_system.unit_for!(:fireball)
data/test/parser_test.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::ParserTest < ActiveSupport::TestCase
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class AlwaysTrueCache
2
3
  def exist?
3
4
  true
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  Magic = Measured.build do
2
3
  unit :magic_missile, aliases: [:magic_missiles, "magic missile"]
3
4
  unit :fireball, value: "2/3 magic_missile", aliases: [:fire, :fireballs]
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Extract the subclasses that exist early on because other classes will get added by tests
2
3
  # later on in execution and in an unpredictable order.
3
4
  class ActiveSupport::TestCase
data/test/test_helper.rb CHANGED
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require "pry" unless ENV["CI"]
2
3
  require "measured"
3
4
  require "minitest/reporters"
4
5
  require "minitest/autorun"
5
- require "mocha/setup"
6
+ require "mocha/minitest"
6
7
 
7
8
  ActiveSupport.test_order = :random
8
9
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::UnitErrorTest < ActiveSupport::TestCase
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::UnitSystemBuilderTest < ActiveSupport::TestCase
@@ -11,15 +12,17 @@ class Measured::UnitSystemBuilderTest < ActiveSupport::TestCase
11
12
  end
12
13
 
13
14
  test "#unit cannot add duplicate unit names" do
14
- assert_raises Measured::UnitError do
15
+ error = assert_raises Measured::UnitAlreadyAdded do
15
16
  Measured.build do
16
17
  unit :m
17
18
  unit :in, aliases: [:inch], value: "0.0254 m"
18
19
  unit :in, aliases: [:thing], value: "123 m"
19
20
  end
20
21
  end
22
+ assert_equal("in", error.unit_name)
23
+ assert_equal("Unit in has already been added.", error.message)
21
24
 
22
- assert_raises Measured::UnitError do
25
+ assert_raises Measured::UnitAlreadyAdded do
23
26
  Measured.build do
24
27
  unit :m
25
28
  unit :in, aliases: [:inch], value: "0.0254 m"
@@ -28,6 +31,19 @@ class Measured::UnitSystemBuilderTest < ActiveSupport::TestCase
28
31
  end
29
32
  end
30
33
 
34
+ test "#unit raises when cannot find conversion path" do
35
+ error = assert_raises Measured::MissingConversionPath do
36
+ Measured.build do
37
+ unit :m
38
+ unit :in, value: "0.0254 m"
39
+ unit :pallets, value: "5 cases"
40
+ end
41
+ end
42
+ assert_equal("pallets", error.from)
43
+ assert_equal("m", error.to)
44
+ assert_equal("Cannot find conversion path from pallets to m.", error.message)
45
+ end
46
+
31
47
  test "#unit is case sensitive" do
32
48
  measurable = Measured.build do
33
49
  unit :normal
@@ -38,6 +54,16 @@ class Measured::UnitSystemBuilderTest < ActiveSupport::TestCase
38
54
  assert_equal 'BOLD', measurable.unit_system.unit_for!(:BOLD).name
39
55
  end
40
56
 
57
+ test "#unit traverses aliases" do
58
+ measurable = Measured.build do
59
+ unit :piece, aliases: [:pieces]
60
+ unit :dozen, aliases: [:dz], value: "12 pieces"
61
+ end
62
+
63
+ assert_equal 12, measurable.unit_system.units.last.conversion_amount
64
+ assert_equal "piece", measurable.unit_system.units.last.conversion_unit
65
+ end
66
+
41
67
  test "#si_unit adds 21 new units" do
42
68
  measurable = Measured.build do
43
69
  unit :ft
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::UnitSystemTest < ActiveSupport::TestCase
data/test/unit_test.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::UnitTest < ActiveSupport::TestCase
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::LengthTest < ActiveSupport::TestCase
@@ -142,7 +143,7 @@ class Measured::LengthTest < ActiveSupport::TestCase
142
143
  end
143
144
 
144
145
  test ".convert_to from km to mi" do
145
- assert_conversion Measured::Length, "2000 km", "0.1242742384475E4 mi"
146
+ assert_conversion Measured::Length, "2000 km", "0.1242742384475E4 mi"
146
147
  end
147
148
 
148
149
  test ".convert_to from km to mm" do
@@ -150,7 +151,7 @@ class Measured::LengthTest < ActiveSupport::TestCase
150
151
  end
151
152
 
152
153
  test ".convert_to from km to yd" do
153
- assert_conversion Measured::Length, "2000 km", "0.218722659667542E7 yd"
154
+ assert_conversion Measured::Length, "2000 km", "0.218722659667542E7 yd"
154
155
  end
155
156
 
156
157
  test ".convert_to from m to cm" do
@@ -206,7 +207,7 @@ class Measured::LengthTest < ActiveSupport::TestCase
206
207
  end
207
208
 
208
209
  test ".convert_to from mi to mi" do
209
- assert_exact_conversion Measured::Length, "2000 mi", "2000 mi"
210
+ assert_exact_conversion Measured::Length, "2000 mi", "2000 mi"
210
211
  end
211
212
 
212
213
  test ".convert_to from mi to mm" do
@@ -214,7 +215,7 @@ class Measured::LengthTest < ActiveSupport::TestCase
214
215
  end
215
216
 
216
217
  test ".convert_to from mi to yd" do
217
- assert_exact_conversion Measured::Length, "2000 mi", "3520000 yd"
218
+ assert_exact_conversion Measured::Length, "2000 mi", "3520000 yd"
218
219
  end
219
220
 
220
221
  test ".convert_to from mm to cm" do
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::VolumeTest < ActiveSupport::TestCase
@@ -68,7 +69,7 @@ class Measured::VolumeTest < ActiveSupport::TestCase
68
69
  test ".unit_names should be the list of base unit names" do
69
70
  expected_units = %w(l m3 ft3 in3 gal us_gal qt us_qt pt us_pt oz us_oz)
70
71
  expected_units += Measured::UnitSystemBuilder::SI_PREFIXES.map { |short, _, _| "#{short}l" }
71
- assert_equal expected_units.sort, Measured::Volume.unit_names
72
+ assert_equal expected_units.sort, Measured::Volume.unit_names
72
73
  end
73
74
 
74
75
  test ".name" do
@@ -95,7 +96,7 @@ class Measured::VolumeTest < ActiveSupport::TestCase
95
96
  test ".convert_to from gal to gal" do
96
97
  assert_conversion Measured::Volume, "2000 gal", "2000 gal"
97
98
  end
98
-
99
+
99
100
  test ".convert_to from us_gal to us_gal" do
100
101
  assert_conversion Measured::Volume, "2000 us_gal", "2000 us_gal"
101
102
  end
@@ -123,7 +124,7 @@ class Measured::VolumeTest < ActiveSupport::TestCase
123
124
  test ".convert_to from us_oz to us_oz" do
124
125
  assert_conversion Measured::Volume, "2000 us_oz", "2000 us_oz"
125
126
  end
126
-
127
+
127
128
  test ".convert_to from ml to m3" do
128
129
  assert_conversion Measured::Volume, "2000 ml", "0.002 m3"
129
130
  end
@@ -303,7 +304,7 @@ class Measured::VolumeTest < ActiveSupport::TestCase
303
304
  test ".convert_to from gal to us_oz" do
304
305
  assert_conversion Measured::Volume, "2 gal", "307.4431809292 us_oz"
305
306
  end
306
-
307
+
307
308
  test ".convert_to from us_gal to qt" do
308
309
  assert_conversion Measured::Volume, "2 us_gal", "6.661393477 qt"
309
310
  end
@@ -387,4 +388,4 @@ class Measured::VolumeTest < ActiveSupport::TestCase
387
388
  test ".convert_to from oz to us_oz" do
388
389
  assert_conversion Measured::Volume, "2 oz", "1.9215207864 us_oz"
389
390
  end
390
- end
391
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::WeightTest < ActiveSupport::TestCase
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: measured
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin McPhillips
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2019-05-21 00:00:00.000000000 Z
13
+ date: 2021-05-07 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -18,14 +18,14 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: '5.0'
21
+ version: '5.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '5.0'
28
+ version: '5.2'
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: rake
31
31
  requirement: !ruby/object:Gem::Requirement
@@ -72,16 +72,16 @@ dependencies:
72
72
  name: mocha
73
73
  requirement: !ruby/object:Gem::Requirement
74
74
  requirements:
75
- - - ">"
75
+ - - ">="
76
76
  - !ruby/object:Gem::Version
77
- version: 1.1.0
77
+ version: 1.4.0
78
78
  type: :development
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
81
81
  requirements:
82
- - - ">"
82
+ - - ">="
83
83
  - !ruby/object:Gem::Version
84
- version: 1.1.0
84
+ version: 1.4.0
85
85
  - !ruby/object:Gem::Dependency
86
86
  name: pry
87
87
  requirement: !ruby/object:Gem::Requirement
@@ -104,8 +104,9 @@ executables: []
104
104
  extensions: []
105
105
  extra_rdoc_files: []
106
106
  files:
107
+ - ".github/workflows/ci.yml"
107
108
  - ".gitignore"
108
- - ".travis.yml"
109
+ - CHANGELOG.md
109
110
  - Gemfile
110
111
  - LICENSE
111
112
  - README.md
@@ -115,9 +116,9 @@ files:
115
116
  - cache/volume.json
116
117
  - cache/weight.json
117
118
  - dev.yml
118
- - gemfiles/activesupport-5.0.gemfile
119
- - gemfiles/activesupport-5.1.gemfile
119
+ - gemfiles/activesupport-5.2.gemfile
120
120
  - gemfiles/activesupport-6.0.gemfile
121
+ - gemfiles/activesupport-6.1.gemfile
121
122
  - lib/measured.rb
122
123
  - lib/measured/arithmetic.rb
123
124
  - lib/measured/base.rb
@@ -125,9 +126,13 @@ files:
125
126
  - lib/measured/cache/json_writer.rb
126
127
  - lib/measured/cache/null.rb
127
128
  - lib/measured/conversion_table_builder.rb
129
+ - lib/measured/cycle_detected.rb
128
130
  - lib/measured/measurable.rb
131
+ - lib/measured/missing_conversion_path.rb
129
132
  - lib/measured/parser.rb
130
133
  - lib/measured/unit.rb
134
+ - lib/measured/unit_already_added.rb
135
+ - lib/measured/unit_error.rb
131
136
  - lib/measured/unit_system.rb
132
137
  - lib/measured/unit_system_builder.rb
133
138
  - lib/measured/units/length.rb
@@ -158,7 +163,8 @@ files:
158
163
  homepage: https://github.com/Shopify/measured
159
164
  licenses:
160
165
  - MIT
161
- metadata: {}
166
+ metadata:
167
+ allowed_push_host: https://rubygems.org
162
168
  post_install_message:
163
169
  rdoc_options: []
164
170
  require_paths:
@@ -174,8 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
180
  - !ruby/object:Gem::Version
175
181
  version: '0'
176
182
  requirements: []
177
- rubyforge_project:
178
- rubygems_version: 2.7.6
183
+ rubygems_version: 3.0.3
179
184
  signing_key:
180
185
  specification_version: 4
181
186
  summary: Encapsulate measurements with their units in Ruby
data/.travis.yml DELETED
@@ -1,21 +0,0 @@
1
- language: ruby
2
- sudo: false
3
- cache: bundler
4
- rvm:
5
- - 2.3.8
6
- - 2.4.5
7
- - 2.5.5
8
- - 2.6.3
9
- gemfile:
10
- - Gemfile
11
- - gemfiles/activesupport-5.0.gemfile
12
- - gemfiles/activesupport-5.1.gemfile
13
- - gemfiles/activesupport-6.0.gemfile
14
- before_script:
15
- - gem update --system
16
- matrix:
17
- exclude:
18
- - gemfile: gemfiles/activesupport-6.0.gemfile
19
- rvm: 2.3.8
20
- - gemfile: gemfiles/activesupport-6.0.gemfile
21
- rvm: 2.4.5