measured 2.4.0 → 2.5.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.

Potentially problematic release.


This version of measured might be problematic. Click here for more details.

data/dev.yml CHANGED
@@ -2,7 +2,7 @@ name: measured
2
2
 
3
3
  up:
4
4
  - ruby:
5
- version: 2.3.1
5
+ version: 2.6.3
6
6
  - bundler
7
7
 
8
8
  commands:
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec path: '..'
4
4
 
5
- gem 'activesupport', '~> 4.2'
5
+ gem 'activesupport', '~> 5.0'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', '~> 5.1'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', '~> 6.0.0.rc1'
data/lib/measured/base.rb CHANGED
@@ -2,6 +2,7 @@ require "forwardable"
2
2
  require "measured/version"
3
3
  require "active_support/all"
4
4
  require "bigdecimal"
5
+ require "json"
5
6
 
6
7
  module Measured
7
8
  class UnitError < StandardError ; end
@@ -44,4 +45,7 @@ require "measured/unit"
44
45
  require "measured/unit_system"
45
46
  require "measured/unit_system_builder"
46
47
  require "measured/conversion_table_builder"
48
+ require "measured/cache/null"
49
+ require "measured/cache/json_writer"
50
+ require "measured/cache/json"
47
51
  require "measured/measurable"
@@ -0,0 +1,48 @@
1
+ module Measured::Cache
2
+ class Json
3
+ attr_reader :filename, :path
4
+
5
+ def initialize(filename)
6
+ @filename = filename
7
+ @path = Pathname.new(File.join(File.dirname(__FILE__), "../../../cache", @filename)).cleanpath
8
+ end
9
+
10
+ def exist?
11
+ File.exist?(@path)
12
+ end
13
+
14
+ def read
15
+ return unless exist?
16
+ decode(JSON.load(File.read(@path)))
17
+ end
18
+
19
+ def write(table)
20
+ raise ArgumentError, "Cannot overwrite file cache at runtime."
21
+ end
22
+
23
+ private
24
+
25
+ # JSON dump and load of Rational objects exists, but it changes the behaviour of JSON globally if required.
26
+ # Instead, the same marshalling technique is rewritten here to prevent changing this behaviour project wide.
27
+ # https://github.com/ruby/ruby/blob/trunk/ext/json/lib/json/add/rational.rb
28
+ def encode(table)
29
+ table.each_with_object(table.dup) do |(k1, v1), accu|
30
+ v1.each do |k2, v2|
31
+ if v2.is_a?(Rational)
32
+ accu[k1][k2] = { "numerator" => v2.numerator, "denominator" => v2.denominator }
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ 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"])
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ module Measured::Cache::JsonWriter
2
+ def write(table)
3
+ File.open(@path, "w") do |f|
4
+ f.write("// Do not modify this file directly. Regenerate it with 'rake cache:write'.\n")
5
+ f.write(JSON.pretty_generate(encode(table)))
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module Measured::Cache
2
+ class Null
3
+ def exist?
4
+ false
5
+ end
6
+
7
+ def read
8
+ nil
9
+ end
10
+
11
+ def write(table)
12
+ nil
13
+ end
14
+ end
15
+ end
@@ -1,14 +1,29 @@
1
1
  class Measured::ConversionTableBuilder
2
2
  attr_reader :units
3
3
 
4
- def initialize(units)
4
+ def initialize(units, cache: nil)
5
5
  @units = units
6
+ cache ||= { class: Measured::Cache::Null }
7
+ @cache = cache[:class].new(*cache[:args])
6
8
  end
7
9
 
8
10
  def to_h
9
- table = {}
11
+ return @cache.read if cached?
12
+ generate_table
13
+ end
14
+
15
+ def update_cache
16
+ @cache.write(generate_table)
17
+ end
18
+
19
+ def cached?
20
+ @cache.exist?
21
+ end
10
22
 
11
- units.map{|u| u.name}.each do |to_unit|
23
+ private
24
+
25
+ def generate_table
26
+ units.map(&:name).each_with_object({}) do |to_unit, table|
12
27
  to_table = {to_unit => Rational(1, 1)}
13
28
 
14
29
  table.each do |from_unit, from_table|
@@ -19,12 +34,8 @@ class Measured::ConversionTableBuilder
19
34
 
20
35
  table[to_unit] = to_table
21
36
  end
22
-
23
- table
24
37
  end
25
38
 
26
- private
27
-
28
39
  def find_conversion(to:, from:)
29
40
  conversion = find_direct_conversion_cached(to: to, from: from) || find_tree_traversal_conversion(to: to, from: from)
30
41
 
@@ -34,13 +45,13 @@ class Measured::ConversionTableBuilder
34
45
  end
35
46
 
36
47
  def find_direct_conversion_cached(to:, from:)
37
- @cache ||= {}
38
- @cache[to] ||= {}
48
+ @direct_conversion_cache ||= {}
49
+ @direct_conversion_cache[to] ||= {}
39
50
 
40
- if @cache[to].key?(from)
41
- @cache[to][from]
51
+ if @direct_conversion_cache[to].key?(from)
52
+ @direct_conversion_cache[to][from]
42
53
  else
43
- @cache[to][from] = find_direct_conversion(to: to, from: from)
54
+ @direct_conversion_cache[to][from] = find_direct_conversion(to: to, from: from)
44
55
  end
45
56
  end
46
57
 
@@ -1,9 +1,9 @@
1
1
  class Measured::UnitSystem
2
2
  attr_reader :units
3
3
 
4
- def initialize(units)
4
+ def initialize(units, cache: nil)
5
5
  @units = units.map { |unit| unit.with_unit_system(self) }
6
- @conversion_table_builder = Measured::ConversionTableBuilder.new(@units)
6
+ @conversion_table_builder = Measured::ConversionTableBuilder.new(@units, cache: cache)
7
7
  end
8
8
 
9
9
  def unit_names_with_aliases
@@ -41,6 +41,14 @@ class Measured::UnitSystem
41
41
  value.to_r * conversion
42
42
  end
43
43
 
44
+ def update_cache
45
+ @conversion_table_builder.update_cache
46
+ end
47
+
48
+ def cached?
49
+ @conversion_table_builder.cached?
50
+ end
51
+
44
52
  protected
45
53
 
46
54
  def conversion_table
@@ -1,6 +1,7 @@
1
1
  class Measured::UnitSystemBuilder
2
2
  def initialize
3
3
  @units = []
4
+ @cache = nil
4
5
  end
5
6
 
6
7
  def unit(unit_name, aliases: [], value: nil)
@@ -13,8 +14,13 @@ class Measured::UnitSystemBuilder
13
14
  nil
14
15
  end
15
16
 
17
+ def cache(cache_class, *args)
18
+ @cache = {class: cache_class, args: args}
19
+ nil
20
+ end
21
+
16
22
  def build
17
- Measured::UnitSystem.new(@units)
23
+ Measured::UnitSystem.new(@units, cache: @cache)
18
24
  end
19
25
 
20
26
  private
@@ -5,4 +5,6 @@ Measured::Length = Measured.build do
5
5
  unit :ft, value: "12 in", aliases: [:foot, :feet]
6
6
  unit :yd, value: "3 ft", aliases: [:yard, :yards]
7
7
  unit :mi, value: "5280 ft", aliases: [:mile, :miles]
8
+
9
+ cache Measured::Cache::Json, "length.json"
8
10
  end
@@ -12,4 +12,6 @@ Measured::Volume = Measured.build do
12
12
  unit :us_pt, value: "0.125 us_gal", aliases: [:us_pint, :us_pints]
13
13
  unit :oz, value: "0.00625 gal", aliases: [:fl_oz, :imp_fl_oz, :imperial_fluid_ounce, :imperial_fluid_ounces]
14
14
  unit :us_oz, value: "0.0078125 us_gal", aliases: [:us_fl_oz, :us_fluid_ounce, :us_fluid_ounces]
15
- end
15
+
16
+ cache Measured::Cache::Json, "volume.json"
17
+ end
@@ -8,4 +8,6 @@ Measured::Weight = Measured.build do
8
8
  unit :short_ton, value: "2000 lb", aliases: [:short_tons]
9
9
  unit :lb, value: "0.45359237 kg", aliases: [:lbs, :pound, :pounds]
10
10
  unit :oz, value: "1/16 lb", aliases: [:ounce, :ounces]
11
+
12
+ cache Measured::Cache::Json, "weight.json"
11
13
  end
@@ -1,3 +1,3 @@
1
1
  module Measured
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
data/measured.gemspec CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency "activesupport", ">= 4.2"
21
+ spec.add_runtime_dependency "activesupport", ">= 5.0"
22
22
 
23
23
  spec.add_development_dependency "rake", "> 10.0"
24
24
  spec.add_development_dependency "minitest", "> 5.5.1"
@@ -0,0 +1,42 @@
1
+ require "test_helper"
2
+
3
+ class Measured::Cache::JsonTest < ActiveSupport::TestCase
4
+ setup do
5
+ @cache = Measured::Cache::Json.new("test.json")
6
+ @table_json = { "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } }.to_json
7
+ @table_hash = { "a" => { "b" => Rational(2, 3) } }
8
+ end
9
+
10
+ test "#initialize sets the filename and path" do
11
+ assert_equal "test.json", @cache.filename
12
+ assert_match(/.+\/cache\/test\.json$/, @cache.path.to_s)
13
+ refute_match "../", @cache.path.to_s
14
+ end
15
+
16
+ test "#exist? returns false if the file does not exist" do
17
+ File.expects(:exist?).with(@cache.path).returns(false)
18
+ refute_predicate @cache, :exist?
19
+ end
20
+
21
+ test "#exist? returns true if the file exists" do
22
+ File.expects(:exist?).with(@cache.path).returns(true)
23
+ assert_predicate @cache, :exist?
24
+ end
25
+
26
+ test "#read returns nil if the file does not exist" do
27
+ File.expects(:exist?).with(@cache.path).returns(false)
28
+ assert_nil @cache.read
29
+ end
30
+
31
+ test "#read loads the file if it exists" do
32
+ File.expects(:exist?).with(@cache.path).returns(true)
33
+ File.expects(:read).with(@cache.path).returns(@table_json)
34
+ assert_equal @table_hash, @cache.read
35
+ end
36
+
37
+ test "#write raises not implemented" do
38
+ assert_raises(ArgumentError) do
39
+ @cache.write({})
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ require "test_helper"
2
+
3
+ class Measured::Cache::JsonWriterTest < ActiveSupport::TestCase
4
+ class JsonTestWithWriter < Measured::Cache::Json
5
+ prepend Measured::Cache::JsonWriter
6
+ end
7
+
8
+ setup do
9
+ @cache = JsonTestWithWriter.new("test.json")
10
+ @table_json = JSON.pretty_generate({ "a" => { "b" => { "numerator" => 2, "denominator" => 3 } } })
11
+ @table_hash = { "a" => { "b" => Rational(2, 3) } }
12
+ end
13
+
14
+ test "#write writes the file" do
15
+ f = stub
16
+ f.expects(:write).with("// Do not modify this file directly. Regenerate it with 'rake cache:write'.\n")
17
+ f.expects(:write).with(@table_json)
18
+
19
+ File.expects(:open).with(@cache.path, "w").returns(123).yields(f)
20
+ assert_equal 123, @cache.write(@table_hash)
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ require "test_helper"
2
+
3
+ class Measured::Cache::NullTest < ActiveSupport::TestCase
4
+ setup do
5
+ @cache = Measured::Cache::Null.new
6
+ end
7
+
8
+ test "#exist? false" do
9
+ assert_equal false, @cache.exist?
10
+ end
11
+
12
+ test "#read returns nil" do
13
+ assert_nil @cache.read
14
+ end
15
+
16
+ test "#write returns nil" do
17
+ assert_nil @cache.write({})
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ require "test_helper"
2
+
3
+ # In general we do not want to expose the inner workings of the caching and unit systems as part of the API.
4
+ # 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
5
+ # in the unit systems. So we compromise by reaching into implementation to force compare them.
6
+ class Measured::CacheConsistencyTest < ActiveSupport::TestCase
7
+ measurable_subclasses.select { |m| m.unit_system.cached? }.each do |measurable|
8
+ test "cached measurable #{ measurable } is not out of sync with the definition" do
9
+ builder = measurable.unit_system.instance_variable_get("@conversion_table_builder")
10
+ cache = builder.instance_variable_get("@cache")
11
+
12
+ expected = builder.send(:generate_table)
13
+ actual = cache.read
14
+
15
+ 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."
16
+ end
17
+ end
18
+ end
@@ -115,4 +115,22 @@ class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
115
115
  assert_instance_of Rational, value
116
116
  end
117
117
  end
118
+
119
+ test "#cached? returns true if there's a cache" do
120
+ builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
121
+ assert_predicate builder, :cached?
122
+ end
123
+
124
+ test "#cached? returns false if there is not a cache" do
125
+ builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)])
126
+ refute_predicate builder, :cached?
127
+ end
128
+
129
+ test "#write_cache pushes the generated table into the cache and writes it" do
130
+ builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
131
+ AlwaysTrueCache.any_instance.expects(:exist?).returns(false)
132
+ table = builder.to_h
133
+ AlwaysTrueCache.any_instance.expects(:write).with(table).returns(123)
134
+ assert_equal 123, builder.update_cache
135
+ end
118
136
  end
@@ -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