measured 2.5.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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