measured 2.3.0 → 2.6.0

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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +31 -0
  3. data/CHANGELOG.md +25 -0
  4. data/README.md +12 -10
  5. data/Rakefile +22 -1
  6. data/cache/.keep +0 -0
  7. data/cache/length.json +2553 -0
  8. data/cache/volume.json +4163 -0
  9. data/cache/weight.json +3195 -0
  10. data/dev.yml +2 -2
  11. data/gemfiles/{activesupport-4.2.gemfile → activesupport-5.2.gemfile} +1 -1
  12. data/gemfiles/activesupport-6.0.gemfile +5 -0
  13. data/gemfiles/activesupport-6.1.gemfile +5 -0
  14. data/lib/measured.rb +1 -0
  15. data/lib/measured/arithmetic.rb +1 -0
  16. data/lib/measured/base.rb +12 -6
  17. data/lib/measured/cache/json.rb +49 -0
  18. data/lib/measured/cache/json_writer.rb +9 -0
  19. data/lib/measured/cache/null.rb +16 -0
  20. data/lib/measured/conversion_table_builder.rb +91 -0
  21. data/lib/measured/measurable.rb +18 -21
  22. data/lib/measured/missing_conversion_path.rb +12 -0
  23. data/lib/measured/parser.rb +1 -0
  24. data/lib/measured/unit.rb +16 -26
  25. data/lib/measured/unit_already_added.rb +11 -0
  26. data/lib/measured/unit_error.rb +4 -0
  27. data/lib/measured/unit_system.rb +26 -21
  28. data/lib/measured/unit_system_builder.rb +11 -4
  29. data/lib/measured/units/length.rb +3 -0
  30. data/lib/measured/units/volume.rb +4 -1
  31. data/lib/measured/units/weight.rb +3 -0
  32. data/lib/measured/version.rb +2 -1
  33. data/measured.gemspec +13 -5
  34. data/test/arithmetic_test.rb +1 -0
  35. data/test/cache/json_test.rb +43 -0
  36. data/test/cache/json_writer_test.rb +23 -0
  37. data/test/cache/null_test.rb +20 -0
  38. data/test/cache_consistency_test.rb +19 -0
  39. data/test/conversion_table_builder_test.rb +137 -0
  40. data/test/measurable_test.rb +5 -5
  41. data/test/parser_test.rb +1 -0
  42. data/test/support/always_true_cache.rb +14 -0
  43. data/test/support/fake_system.rb +1 -0
  44. data/test/support/subclasses.rb +7 -0
  45. data/test/test_helper.rb +5 -3
  46. data/test/unit_error_test.rb +1 -0
  47. data/test/unit_system_builder_test.rb +49 -3
  48. data/test/unit_system_test.rb +15 -0
  49. data/test/unit_test.rb +5 -0
  50. data/test/units/length_test.rb +5 -4
  51. data/test/units/volume_test.rb +6 -5
  52. data/test/units/weight_test.rb +1 -0
  53. metadata +45 -18
  54. data/.travis.yml +0 -17
  55. data/lib/measured/conversion_table.rb +0 -65
  56. data/test/conversion_table_test.rb +0 -98
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::UnitSystemBuilder
2
3
  def initialize
3
4
  @units = []
5
+ @cache = nil
4
6
  end
5
7
 
6
8
  def unit(unit_name, aliases: [], value: nil)
@@ -13,8 +15,13 @@ class Measured::UnitSystemBuilder
13
15
  nil
14
16
  end
15
17
 
18
+ def cache(cache_class, *args)
19
+ @cache = {class: cache_class, args: args}
20
+ nil
21
+ end
22
+
16
23
  def build
17
- Measured::UnitSystem.new(@units)
24
+ Measured::UnitSystem.new(@units, cache: @cache)
18
25
  end
19
26
 
20
27
  private
@@ -39,8 +46,8 @@ class Measured::UnitSystemBuilder
39
46
  ["P", "peta", 15],
40
47
  ["E", "exa", 18],
41
48
  ["Z", "zetta", 21],
42
- ["Y", "yotta", 24]
43
- ]
49
+ ["Y", "yotta", 24],
50
+ ].map(&:freeze).freeze
44
51
 
45
52
  def build_si_units(name, aliases: [], value: nil)
46
53
  si_units = [build_unit(name, aliases: aliases, value: value)]
@@ -60,7 +67,7 @@ class Measured::UnitSystemBuilder
60
67
  def check_for_duplicate_unit_names!(unit)
61
68
  names = @units.flat_map(&:names)
62
69
  if names.any? { |name| unit.names.include?(name) }
63
- raise Measured::UnitError, "Unit #{unit.name} has already been added."
70
+ raise Measured::UnitAlreadyAdded.new(unit.name)
64
71
  end
65
72
  end
66
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
 
@@ -5,4 +6,6 @@ Measured::Length = Measured.build do
5
6
  unit :ft, value: "12 in", aliases: [:foot, :feet]
6
7
  unit :yd, value: "3 ft", aliases: [:yard, :yards]
7
8
  unit :mi, value: "5280 ft", aliases: [:mile, :miles]
9
+
10
+ cache Measured::Cache::Json, "length.json"
8
11
  end
@@ -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
 
@@ -12,4 +13,6 @@ Measured::Volume = Measured.build do
12
13
  unit :us_pt, value: "0.125 us_gal", aliases: [:us_pint, :us_pints]
13
14
  unit :oz, value: "0.00625 gal", aliases: [:fl_oz, :imp_fl_oz, :imperial_fluid_ounce, :imperial_fluid_ounces]
14
15
  unit :us_oz, value: "0.0078125 us_gal", aliases: [:us_fl_oz, :us_fluid_ounce, :us_fluid_ounces]
15
- end
16
+
17
+ cache Measured::Cache::Json, "volume.json"
18
+ end
@@ -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
 
@@ -8,4 +9,6 @@ Measured::Weight = Measured.build do
8
9
  unit :short_ton, value: "2000 lb", aliases: [:short_tons]
9
10
  unit :lb, value: "0.45359237 kg", aliases: [:lbs, :pound, :pounds]
10
11
  unit :oz, value: "1/16 lb", aliases: [:ounce, :ounces]
12
+
13
+ cache Measured::Cache::Json, "weight.json"
11
14
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured
2
- VERSION = "2.3.0"
3
+ VERSION = "2.6.0"
3
4
  end
@@ -6,23 +6,31 @@ require 'measured/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "measured"
8
8
  spec.version = Measured::VERSION
9
- spec.authors = ["Kevin McPhillips"]
10
- spec.email = ["github@kevinmcphillips.ca"]
9
+ spec.authors = ["Kevin McPhillips", "Jason Gedge", "Javier Honduvilla Coto"]
10
+ spec.email = ["gems@shopify.com"]
11
11
  spec.summary = %q{Encapsulate measurements with their units in Ruby}
12
- spec.description = %q{Wrapper objects which encapsulate measurments and their associated units in Ruby.}
12
+ spec.description = %q{Wrapper objects which encapsulate measurements and their associated units in Ruby.}
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", ">= 4.2"
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
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require "test_helper"
3
+
4
+ class Measured::Cache::JsonTest < ActiveSupport::TestCase
5
+ setup do
6
+ @cache = Measured::Cache::Json.new("test.json")
7
+ @table_json = { "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } }.to_json
8
+ @table_hash = { "a" => { "b" => Rational(2, 3) } }
9
+ end
10
+
11
+ test "#initialize sets the filename and path" do
12
+ assert_equal "test.json", @cache.filename
13
+ assert_match(/.+\/cache\/test\.json$/, @cache.path.to_s)
14
+ refute_match "../", @cache.path.to_s
15
+ end
16
+
17
+ test "#exist? returns false if the file does not exist" do
18
+ File.expects(:exist?).with(@cache.path).returns(false)
19
+ refute_predicate @cache, :exist?
20
+ end
21
+
22
+ test "#exist? returns true if the file exists" do
23
+ File.expects(:exist?).with(@cache.path).returns(true)
24
+ assert_predicate @cache, :exist?
25
+ end
26
+
27
+ test "#read returns nil if the file does not exist" do
28
+ File.expects(:exist?).with(@cache.path).returns(false)
29
+ assert_nil @cache.read
30
+ end
31
+
32
+ test "#read loads the file if it exists" do
33
+ File.expects(:exist?).with(@cache.path).returns(true)
34
+ File.expects(:read).with(@cache.path).returns(@table_json)
35
+ assert_equal @table_hash, @cache.read
36
+ end
37
+
38
+ test "#write raises not implemented" do
39
+ assert_raises(ArgumentError) do
40
+ @cache.write({})
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ require "test_helper"
3
+
4
+ class Measured::Cache::JsonWriterTest < ActiveSupport::TestCase
5
+ class JsonTestWithWriter < Measured::Cache::Json
6
+ prepend Measured::Cache::JsonWriter
7
+ end
8
+
9
+ setup do
10
+ @cache = JsonTestWithWriter.new("test.json")
11
+ @table_json = JSON.pretty_generate({ "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } })
12
+ @table_hash = { "a" => { "b" => Rational(2, 3) } }
13
+ end
14
+
15
+ test "#write writes the file" do
16
+ f = stub
17
+ f.expects(:write).with("// Do not modify this file directly. Regenerate it with 'rake cache:write'.\n")
18
+ f.expects(:write).with(@table_json)
19
+
20
+ File.expects(:open).with(@cache.path, "w").returns(123).yields(f)
21
+ assert_equal 123, @cache.write(@table_hash)
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require "test_helper"
3
+
4
+ class Measured::Cache::NullTest < ActiveSupport::TestCase
5
+ setup do
6
+ @cache = Measured::Cache::Null.new
7
+ end
8
+
9
+ test "#exist? false" do
10
+ assert_equal false, @cache.exist?
11
+ end
12
+
13
+ test "#read returns nil" do
14
+ assert_nil @cache.read
15
+ end
16
+
17
+ test "#write returns nil" do
18
+ assert_nil @cache.write({})
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ require "test_helper"
3
+
4
+ # In general we do not want to expose the inner workings of the caching and unit systems as part of the API.
5
+ # But we do want to be able to test that there is no conflict between what's in the cache file and what is defined
6
+ # in the unit systems. So we compromise by reaching into implementation to force compare them.
7
+ class Measured::CacheConsistencyTest < ActiveSupport::TestCase
8
+ measurable_subclasses.select { |m| m.unit_system.cached? }.each do |measurable|
9
+ test "cached measurable #{ measurable } is not out of sync with the definition" do
10
+ builder = measurable.unit_system.instance_variable_get("@conversion_table_builder")
11
+ cache = builder.instance_variable_get("@cache")
12
+
13
+ expected = builder.send(:generate_table)
14
+ actual = cache.read
15
+
16
+ assert expected == actual, "The contents of the file cache for `#{ measurable }` does not match what the unit system generated.\nTry running `rake cache:write` to update the caches."
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+ require "test_helper"
3
+
4
+ class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
5
+ test "#initialize creates a new object with the units" do
6
+ units = [Measured::Unit.new(:test)]
7
+
8
+ assert_equal units, Measured::ConversionTableBuilder.new(units).units
9
+ end
10
+
11
+ test "#to_h should return a hash for the simple case" do
12
+ conversion_table = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)]).to_h
13
+
14
+ expected = {
15
+ "test" => {"test" => Rational(1, 1)}
16
+ }
17
+
18
+ assert_equal expected, conversion_table
19
+ assert_instance_of Rational, conversion_table.values.first.values.first
20
+ end
21
+
22
+ test "#to_h returns expected nested hashes with BigDecimal conversion factors in a tiny data set" do
23
+ conversion_table = Measured::ConversionTableBuilder.new([
24
+ Measured::Unit.new(:m),
25
+ Measured::Unit.new(:cm, value: "0.01 m"),
26
+ ]).to_h
27
+
28
+ expected = {
29
+ "m" => {
30
+ "m" => Rational(1, 1),
31
+ "cm" => Rational(100, 1),
32
+ },
33
+ "cm" => {
34
+ "m" => Rational(1, 100),
35
+ "cm" => Rational(1, 1),
36
+ }
37
+ }
38
+
39
+ assert_equal expected, conversion_table
40
+
41
+ conversion_table.values.map(&:values).flatten.each do |value|
42
+ assert_instance_of Rational, value
43
+ end
44
+ end
45
+
46
+ test "#to_h returns expected nested hashes factors" do
47
+ conversion_table = Measured::ConversionTableBuilder.new([
48
+ Measured::Unit.new(:m),
49
+ Measured::Unit.new(:cm, value: "0.01 m"),
50
+ Measured::Unit.new(:mm, value: "0.001 m"),
51
+ ]).to_h
52
+
53
+ expected = {
54
+ "m" => {
55
+ "m" => Rational(1, 1),
56
+ "cm" => Rational(100, 1),
57
+ "mm" => Rational(1000, 1),
58
+ },
59
+ "cm" => {
60
+ "m" => Rational(1, 100),
61
+ "cm" => Rational(1, 1),
62
+ "mm" => Rational(10, 1),
63
+ },
64
+ "mm" => {
65
+ "m" => Rational(1, 1000),
66
+ "cm" => Rational(1, 10),
67
+ "mm" => Rational(1, 1),
68
+ }
69
+ }
70
+
71
+ assert_equal expected, conversion_table
72
+
73
+ conversion_table.values.map(&:values).flatten.each do |value|
74
+ assert_instance_of Rational, value
75
+ end
76
+ end
77
+
78
+ test "#to_h returns expected nested hashes in an indrect path" do
79
+ conversion_table = Measured::ConversionTableBuilder.new([
80
+ Measured::Unit.new(:mm),
81
+ Measured::Unit.new(:cm, value: "10 mm"),
82
+ Measured::Unit.new(:dm, value: "10 cm"),
83
+ Measured::Unit.new(:m, value: "10 dm"),
84
+ ]).to_h
85
+
86
+ expected = {
87
+ "m" => {
88
+ "m" => Rational(1, 1),
89
+ "dm" => Rational(10, 1),
90
+ "cm" => Rational(100, 1),
91
+ "mm" => Rational(1000, 1),
92
+ },
93
+ "cm" => {
94
+ "m" => Rational(1, 100),
95
+ "dm" => Rational(1, 10),
96
+ "cm" => Rational(1, 1),
97
+ "mm" => Rational(10, 1),
98
+ },
99
+ "dm" => {
100
+ "m" => Rational(1, 10),
101
+ "cm" => Rational(10, 1),
102
+ "dm" => Rational(1, 1),
103
+ "mm" => Rational(100, 1),
104
+ },
105
+ "mm" => {
106
+ "m" => Rational(1, 1000),
107
+ "dm" => Rational(1, 100),
108
+ "cm" => Rational(1, 10),
109
+ "mm" => Rational(1, 1),
110
+ }
111
+ }
112
+
113
+ assert_equal expected, conversion_table
114
+
115
+ conversion_table.values.map(&:values).flatten.each do |value|
116
+ assert_instance_of Rational, value
117
+ end
118
+ end
119
+
120
+ test "#cached? returns true if there's a cache" do
121
+ builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
122
+ assert_predicate builder, :cached?
123
+ end
124
+
125
+ test "#cached? returns false if there is not a cache" do
126
+ builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)])
127
+ refute_predicate builder, :cached?
128
+ end
129
+
130
+ test "#write_cache pushes the generated table into the cache and writes it" do
131
+ builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
132
+ AlwaysTrueCache.any_instance.expects(:exist?).returns(false)
133
+ table = builder.to_h
134
+ AlwaysTrueCache.any_instance.expects(:write).with(table).returns(123)
135
+ assert_equal 123, builder.update_cache
136
+ end
137
+ end
@@ -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)
@@ -261,7 +261,7 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
261
261
 
262
262
  test "#<=> doesn't compare against zero" do
263
263
  assert_nil @magic <=> 0
264
- assert_nil @magic <=> BigDecimal.new(0)
264
+ assert_nil @magic <=> BigDecimal(0)
265
265
  assert_nil @magic <=> 0.00
266
266
  end
267
267
 
@@ -279,7 +279,7 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
279
279
  test "#== doesn't compare against zero" do
280
280
  arcane_zero = Magic.new(0, :arcane)
281
281
  refute_equal arcane_zero, 0
282
- refute_equal arcane_zero, BigDecimal.new(0)
282
+ refute_equal arcane_zero, BigDecimal(0)
283
283
  refute_equal arcane_zero, 0.0
284
284
  end
285
285
 
@@ -295,10 +295,10 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
295
295
 
296
296
  test "#> and #< should not compare against zero" do
297
297
  assert_raises(ArgumentError) { @magic > 0 }
298
- assert_raises(ArgumentError) { @magic > BigDecimal.new(0) }
298
+ assert_raises(ArgumentError) { @magic > BigDecimal(0) }
299
299
  assert_raises(ArgumentError) { @magic > 0.00 }
300
300
  assert_raises(ArgumentError) { @magic < 0 }
301
- assert_raises(ArgumentError) { @magic < BigDecimal.new(0) }
301
+ assert_raises(ArgumentError) { @magic < BigDecimal(0) }
302
302
  assert_raises(ArgumentError) { @magic < 0.00 }
303
303
  end
304
304
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
4
  class Measured::ParserTest < ActiveSupport::TestCase
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ class AlwaysTrueCache
3
+ def exist?
4
+ true
5
+ end
6
+
7
+ def read
8
+ {}
9
+ end
10
+
11
+ def write(*)
12
+ nil
13
+ end
14
+ end
@@ -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]
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Extract the subclasses that exist early on because other classes will get added by tests
3
+ # later on in execution and in an unpredictable order.
4
+ class ActiveSupport::TestCase
5
+ cattr_accessor :measurable_subclasses
6
+ self.measurable_subclasses = Measured::Measurable.subclasses
7
+ end