measured 2.5.2 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce0f6c7c10571db0dd0d844638dd0bae637d2f9b13e87c29700bf9db51d84197
4
- data.tar.gz: 936e5b74c36c65ca8393b94684b13341ff84d6981cfa34379af9cd0f8bdbed94
3
+ metadata.gz: 474f40f00059e15ec826a6ac74758ea6919f3b3c61a75bc42bcf181f9ac357dc
4
+ data.tar.gz: 356026fb5443a773928dbe21ac20f28f97ed72a987f6708b682cb665eeb511fc
5
5
  SHA512:
6
- metadata.gz: 95eea761fff7e89716925729c8dae771b6274705390622930cf58443bc1d18bd2405bc65b13fa46b1c232ad249a231b0c1d5d7bedafa5d8262f860d94ac24437
7
- data.tar.gz: b9d5ed9a9dc1a0fe8710a62c6c7d673d53c4fee4161f76452ab1f1b708edcb1a8ca081577dd4fb012358b375952d675f8e0bfc8d253f5b8519a024c944806bf1
6
+ metadata.gz: be4796ff68ae8e99abdd8361c68c79624e5b6b10f143a3314edc834e28cd79aa8c6f2c5707d49564d42c8d80fd0521a3db38023cd688a70e733a8f737991ff22
7
+ data.tar.gz: 56a1cf4956fc1eb5802bd74841dfcef1114eaf3514f1a0c3c3c9585aa7643e0c36f2ec287c8933c879ce4a5ecd2e1232ce2a4ea0e30f7cef7fc1a5cc90db69e8
@@ -0,0 +1,31 @@
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.6'
14
+ - '2.7'
15
+ - '3.0'
16
+ gemfile:
17
+ - Gemfile
18
+ - gemfiles/activesupport-5.2.gemfile
19
+ - gemfiles/activesupport-6.0.gemfile
20
+ - gemfiles/activesupport-6.1.gemfile
21
+ name: Ruby ${{ matrix.ruby }} ${{ matrix.gemfile }}
22
+ steps:
23
+ - uses: actions/checkout@v1
24
+ - name: Set up Ruby ${{ matrix.ruby }}
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ bundler-cache: true
29
+ - name: Run tests
30
+ run: |
31
+ bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ Unreleased
2
+ -----
3
+
4
+ 2.8.0
5
+ -----
6
+
7
+ * Drop support for Ruby 2.5
8
+ * Use Ruby 3.0.2 for development
9
+
10
+ 2.7.1
11
+ -----
12
+
13
+ * Fix Ruby 3.0 compatibility
14
+
15
+ 2.7.0
16
+ -----
17
+
18
+ * Raises an exception on cyclic conversions. (@arturopie)
19
+ * Deduplicate strings loaded from the cache.
20
+ * Deduplicate parsed units.
21
+
22
+ 2.6.0
23
+ -----
24
+
25
+ * Add `Measured::MissingConversionPath` and `Measured::UnitAlreadyAdded` as subclasses of `Measured::UnitError` to handle specific error cases. (@arturopie)
26
+ * Support only ActiveSupport 5.2 and above.
27
+
28
+
1
29
  2.5.2
2
30
  -----
3
31
 
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
 
data/dev.yml CHANGED
@@ -2,7 +2,7 @@ name: measured
2
2
 
3
3
  up:
4
4
  - ruby:
5
- version: 2.6.3
5
+ version: 3.0.2
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', '~> 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/base.rb CHANGED
@@ -6,8 +6,6 @@ require "bigdecimal"
6
6
  require "json"
7
7
 
8
8
  module Measured
9
- class UnitError < StandardError ; end
10
-
11
9
  class << self
12
10
  def build(&block)
13
11
  builder = UnitSystemBuilder.new
@@ -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"
@@ -14,7 +14,7 @@ module Measured::Cache
14
14
 
15
15
  def read
16
16
  return unless exist?
17
- decode(JSON.load(File.read(@path)))
17
+ decode(JSON.load(File.read(@path), nil, freeze: true))
18
18
  end
19
19
 
20
20
  def write(table)
@@ -37,11 +37,17 @@ module Measured::Cache
37
37
  end
38
38
 
39
39
  def decode(table)
40
- table.each_with_object(table.dup) do |(k1, v1), accu|
41
- v1.each do |k2, v2|
42
- if v2.is_a?(Hash)
43
- 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
44
48
  end
49
+ else
50
+ value1
45
51
  end
46
52
  end
47
53
  end
@@ -24,6 +24,8 @@ class Measured::ConversionTableBuilder
24
24
  private
25
25
 
26
26
  def generate_table
27
+ validate_no_cycles
28
+
27
29
  units.map(&:name).each_with_object({}) do |to_unit, table|
28
30
  to_table = {to_unit => Rational(1, 1)}
29
31
 
@@ -37,10 +39,27 @@ class Measured::ConversionTableBuilder
37
39
  end
38
40
  end
39
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
+
40
59
  def find_conversion(to:, from:)
41
60
  conversion = find_direct_conversion_cached(to: to, from: from) || find_tree_traversal_conversion(to: to, from: from)
42
61
 
43
- raise Measured::UnitError, "Cannot find conversion path from #{ from } to #{ to }." unless conversion
62
+ raise Measured::MissingConversionPath.new(from, to) unless conversion
44
63
 
45
64
  conversion
46
65
  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
@@ -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
@@ -33,6 +33,6 @@ module Measured::Parser
33
33
 
34
34
  raise Measured::UnitError, "Cannot parse measurement from '#{string}'" unless result
35
35
 
36
- [result.captures[0].to_r, result.captures[1]]
36
+ [result.captures[0].to_r, -result.captures[1]]
37
37
  end
38
38
  end
@@ -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
@@ -67,7 +67,7 @@ class Measured::UnitSystemBuilder
67
67
  def check_for_duplicate_unit_names!(unit)
68
68
  names = @units.flat_map(&:names)
69
69
  if names.any? { |name| unit.names.include?(name) }
70
- raise Measured::UnitError, "Unit #{unit.name} has already been added."
70
+ raise Measured::UnitAlreadyAdded.new(unit.name)
71
71
  end
72
72
  end
73
73
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Measured
3
- VERSION = "2.5.2"
3
+ VERSION = "2.8.0"
4
4
  end
data/measured.gemspec CHANGED
@@ -13,12 +13,20 @@ 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"
@@ -117,6 +117,16 @@ class Measured::ConversionTableBuilderTest < ActiveSupport::TestCase
117
117
  end
118
118
  end
119
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
+
120
130
  test "#cached? returns true if there's a cache" do
121
131
  builder = Measured::ConversionTableBuilder.new([Measured::Unit.new(:test)], cache: { class: AlwaysTrueCache })
122
132
  assert_predicate builder, :cached?
@@ -12,15 +12,17 @@ class Measured::UnitSystemBuilderTest < ActiveSupport::TestCase
12
12
  end
13
13
 
14
14
  test "#unit cannot add duplicate unit names" do
15
- assert_raises Measured::UnitError do
15
+ error = assert_raises Measured::UnitAlreadyAdded do
16
16
  Measured.build do
17
17
  unit :m
18
18
  unit :in, aliases: [:inch], value: "0.0254 m"
19
19
  unit :in, aliases: [:thing], value: "123 m"
20
20
  end
21
21
  end
22
+ assert_equal("in", error.unit_name)
23
+ assert_equal("Unit in has already been added.", error.message)
22
24
 
23
- assert_raises Measured::UnitError do
25
+ assert_raises Measured::UnitAlreadyAdded do
24
26
  Measured.build do
25
27
  unit :m
26
28
  unit :in, aliases: [:inch], value: "0.0254 m"
@@ -29,6 +31,19 @@ class Measured::UnitSystemBuilderTest < ActiveSupport::TestCase
29
31
  end
30
32
  end
31
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
+
32
47
  test "#unit is case sensitive" do
33
48
  measurable = Measured.build do
34
49
  unit :normal
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.2
4
+ version: 2.8.0
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: 2020-01-08 00:00:00.000000000 Z
13
+ date: 2021-11-08 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
@@ -104,8 +104,8 @@ executables: []
104
104
  extensions: []
105
105
  extra_rdoc_files: []
106
106
  files:
107
+ - ".github/workflows/ci.yml"
107
108
  - ".gitignore"
108
- - ".travis.yml"
109
109
  - CHANGELOG.md
110
110
  - Gemfile
111
111
  - LICENSE
@@ -116,9 +116,9 @@ files:
116
116
  - cache/volume.json
117
117
  - cache/weight.json
118
118
  - dev.yml
119
- - gemfiles/activesupport-5.0.gemfile
120
- - gemfiles/activesupport-5.1.gemfile
119
+ - gemfiles/activesupport-5.2.gemfile
121
120
  - gemfiles/activesupport-6.0.gemfile
121
+ - gemfiles/activesupport-6.1.gemfile
122
122
  - lib/measured.rb
123
123
  - lib/measured/arithmetic.rb
124
124
  - lib/measured/base.rb
@@ -126,9 +126,13 @@ files:
126
126
  - lib/measured/cache/json_writer.rb
127
127
  - lib/measured/cache/null.rb
128
128
  - lib/measured/conversion_table_builder.rb
129
+ - lib/measured/cycle_detected.rb
129
130
  - lib/measured/measurable.rb
131
+ - lib/measured/missing_conversion_path.rb
130
132
  - lib/measured/parser.rb
131
133
  - lib/measured/unit.rb
134
+ - lib/measured/unit_already_added.rb
135
+ - lib/measured/unit_error.rb
132
136
  - lib/measured/unit_system.rb
133
137
  - lib/measured/unit_system_builder.rb
134
138
  - lib/measured/units/length.rb
@@ -159,7 +163,8 @@ files:
159
163
  homepage: https://github.com/Shopify/measured
160
164
  licenses:
161
165
  - MIT
162
- metadata: {}
166
+ metadata:
167
+ allowed_push_host: https://rubygems.org
163
168
  post_install_message:
164
169
  rdoc_options: []
165
170
  require_paths:
@@ -175,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
180
  - !ruby/object:Gem::Version
176
181
  version: '0'
177
182
  requirements: []
178
- rubygems_version: 3.0.3
183
+ rubygems_version: 3.2.20
179
184
  signing_key:
180
185
  specification_version: 4
181
186
  summary: Encapsulate measurements with their units in Ruby
data/.travis.yml DELETED
@@ -1,17 +0,0 @@
1
- language: ruby
2
- sudo: false
3
- cache: bundler
4
- rvm:
5
- - 2.4.9
6
- - 2.5.7
7
- - 2.6.5
8
- - 2.7.0
9
- gemfile:
10
- - Gemfile
11
- - gemfiles/activesupport-5.0.gemfile
12
- - gemfiles/activesupport-5.1.gemfile
13
- - gemfiles/activesupport-6.0.gemfile
14
- matrix:
15
- exclude:
16
- - gemfile: gemfiles/activesupport-6.0.gemfile
17
- rvm: 2.4.9