measured 2.6.0 → 2.8.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.

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2de9a5e120aaab53f71956ad225722ed2d5cdd01d018104b214af063eb22b149
4
- data.tar.gz: f65f75ed06c7118f76da05d638782c8b0049f928e6ce0ce96f93e27673d3dd48
3
+ metadata.gz: 5ec3f063994e56291a70f5b2726890e03b43ef00e512894a901bc8ea924ae895
4
+ data.tar.gz: 442e6cca31eb154cbe318287a64f9379ab987b9ddcd59629a2780960b52ea00d
5
5
  SHA512:
6
- metadata.gz: c445792e2670c86ab8526ba7021880c6bef7db3850be768aae8a54ff024b4e56816b24cd4df88f6748971b7e176bb5eceefa48234511d9fbb25f67a065966733
7
- data.tar.gz: 3eae614b358b735a68a6d8a17088f252d7f9db11db3f31c8cbc2a3e4e54d1ac0441d0af2d2f69a44bcbe2654c8839356ac00bc6b7c929d6c3e857f7afbc91d6e
6
+ metadata.gz: 141a1214a375f0af9ae4d92c1e9c51d77e2a98d2a174321ee9e85eb4387fa72c3c98c49dcc5448d6617f7cc7d6bdf67e9ca4f12772d7446cb00e827329108d4f
7
+ data.tar.gz: f5a34f7aa0baf05177f216089dd9b4443ca2b04eecda5c0b72bf5899973a9a5a2a462b2e4f11ea7087dffcc8927e35835e9555da211bc628d50f76fadd110c35
@@ -10,9 +10,9 @@ jobs:
10
10
  strategy:
11
11
  matrix:
12
12
  ruby:
13
- - 2.5
14
- - 2.6
15
- - 2.7
13
+ - '2.6'
14
+ - '2.7'
15
+ - '3.0'
16
16
  gemfile:
17
17
  - Gemfile
18
18
  - gemfiles/activesupport-5.2.gemfile
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ Unreleased
2
+ -----
3
+
4
+
5
+
6
+ 2.8.1
7
+ -----
8
+
9
+ * Allow format without the conversion string. (@saraid)
10
+ * Add special case for zero coercion to allow direct use of `sum` method. (@jmortlock)
11
+ * Add useful numeric methods `finite?`, `infinite?`, `zero?`, `nonzero?`, `positive?` and `negative?`. (@jmortlock)
12
+
13
+ 2.8.0
14
+ -----
15
+
16
+ * Drop support for Ruby 2.5
17
+ * Use Ruby 3.0.2 for development
18
+
19
+ 2.7.1
20
+ -----
21
+
22
+ * Fix Ruby 3.0 compatibility
23
+
24
+ 2.7.0
25
+ -----
26
+
27
+ * Raises an exception on cyclic conversions. (@arturopie)
28
+ * Deduplicate strings loaded from the cache.
29
+ * Deduplicate parsed units.
30
+
1
31
  2.6.0
2
32
  -----
3
33
 
data/README.md CHANGED
@@ -149,6 +149,15 @@ Measured::Weight.new("3.14", "grams").format("%.1<value>f %<unit>s")
149
149
 
150
150
  If no string is passed to the `format` method it defaults to `"%.2<value>f %<unit>s"`.
151
151
 
152
+ If the unit isn't the standard SI unit, it will include a conversion string.
153
+
154
+ ```ruby
155
+ Measured::Weight.new("3.14", "kg").format
156
+ > "3.14 kg (1000/1 g)"
157
+ Measured::Weight.new("3.14", "kg").format(with_conversion_string: false)
158
+ > "3.14 kg"
159
+ ```
160
+
152
161
  ## Units and conversions
153
162
 
154
163
  ### SI units support
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:
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  module Measured::Arithmetic
3
+ extend Forwardable
4
+ def_delegators :@value, :zero?, :positive?, :negative?, :finite?, :infinite?
5
+
3
6
  def +(other)
4
7
  arithmetic_operation(other, :+)
5
8
  end
@@ -19,6 +22,8 @@ module Measured::Arithmetic
19
22
  def coerce(other)
20
23
  if other.is_a?(self.class)
21
24
  [other, self]
25
+ elsif other.is_a?(Numeric) && other.zero?
26
+ [self.class.new(other, self.unit), self]
22
27
  else
23
28
  raise TypeError, "Cannot coerce #{other.class} to #{self.class}"
24
29
  end
@@ -28,6 +33,10 @@ module Measured::Arithmetic
28
33
  raise TypeError, "#{self.class} cannot be converted to an integer"
29
34
  end
30
35
 
36
+ def nonzero?
37
+ value.nonzero? ? self : false
38
+ end
39
+
31
40
  private
32
41
 
33
42
  def arithmetic_operation(other, operator)
data/lib/measured/base.rb CHANGED
@@ -38,6 +38,7 @@ module Measured
38
38
  end
39
39
 
40
40
  require "measured/unit_error"
41
+ require "measured/cycle_detected"
41
42
  require "measured/unit_already_added"
42
43
  require "measured/missing_conversion_path"
43
44
  require "measured/arithmetic"
@@ -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,6 +39,23 @@ 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
 
@@ -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
@@ -43,10 +43,10 @@ class Measured::Measurable < Numeric
43
43
  self.class.new(new_value, new_unit)
44
44
  end
45
45
 
46
- def format(format_string=nil)
46
+ def format(format_string=nil, with_conversion_string: true)
47
47
  kwargs = {
48
48
  value: self.value,
49
- unit: self.unit,
49
+ unit: self.unit.to_s(with_conversion_string: with_conversion_string),
50
50
  }
51
51
  (format_string || DEFAULT_FORMAT_STRING) % kwargs
52
52
  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
data/lib/measured/unit.rb CHANGED
@@ -23,8 +23,8 @@ class Measured::Unit
23
23
  )
24
24
  end
25
25
 
26
- def to_s
27
- if @conversion_string
26
+ def to_s(with_conversion_string: true)
27
+ if with_conversion_string && @conversion_string
28
28
  "#{name} (#{@conversion_string})".freeze
29
29
  else
30
30
  name
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Measured
3
- VERSION = "2.6.0"
3
+ VERSION = "2.8.1"
4
4
  end
@@ -8,6 +8,41 @@ class Measured::ArithmeticTest < ActiveSupport::TestCase
8
8
  @four = Magic.new(4, :magic_missile)
9
9
  end
10
10
 
11
+ test "should be able to sum same units" do
12
+ assert_equal Magic.new(9, :magic_missile), [@two, @three, @four].sum
13
+ end
14
+
15
+ test 'can check for finite?' do
16
+ assert Magic.new(0, :magic_missile).finite?
17
+ refute Magic.new(Float::INFINITY, :magic_missile).finite?
18
+ end
19
+
20
+ test 'can check for infinite?' do
21
+ assert Magic.new(Float::INFINITY, :magic_missile).infinite?
22
+ refute Magic.new(0, :magic_missile).infinite?
23
+ end
24
+
25
+ test 'can check for zero?' do
26
+ assert Magic.new(0, :magic_missile).zero?
27
+ refute Magic.new(1, :magic_missile).zero?
28
+ end
29
+
30
+ test 'can check for nonzero?' do
31
+ assert_equal Magic.new(10, :magic_missile), Magic.new(10, :magic_missile).nonzero?
32
+ assert Magic.new(10, :magic_missile).nonzero?
33
+ refute Magic.new(0, :magic_missile).nonzero?
34
+ end
35
+
36
+ test 'can check for positive?' do
37
+ assert Magic.new(1, :magic_missile).positive?
38
+ refute Magic.new(-1, :magic_missile).positive?
39
+ end
40
+
41
+ test 'can check for negative?' do
42
+ assert Magic.new(-1, :magic_missile).negative?
43
+ refute Magic.new(1, :magic_missile).negative?
44
+ end
45
+
11
46
  test "#+ should add together same units" do
12
47
  assert_equal Magic.new(5, :magic_missile), @two + @three
13
48
  assert_equal Magic.new(5, :magic_missile), @three + @two
@@ -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?
@@ -232,6 +232,11 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
232
232
  assert_equal "9.34 magic_missile", Magic.new(9.342, :magic_missile).format
233
233
  end
234
234
 
235
+ test "#format can drop the conversion amount" do
236
+ assert_equal "1.00 fireball (2/3 magic_missile)", Magic.new(1, :fireball).format
237
+ assert_equal "1.00 fireball", Magic.new(1, :fireball).format(with_conversion_string: false)
238
+ end
239
+
235
240
  test "#humanize outputs the number and the unit properly pluralized" do
236
241
  assert_equal "1 fireball", Magic.new("1", :fire).humanize
237
242
  assert_equal "10 fireballs", Magic.new(10, :fire).humanize
@@ -246,12 +251,6 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
246
251
  assert_equal "#<Magic: 1.234 #<Measured::Unit: magic_missile (magic_missiles, magic missile)>>", Magic.new(1.234, :magic_missile).inspect
247
252
  end
248
253
 
249
- test "#zero? always returns false" do
250
- refute_predicate Magic.new(0, :fire), :zero?
251
- refute_predicate Magic.new(0.0, :fire), :zero?
252
- refute_predicate Magic.new("0.0", :fire), :zero?
253
- end
254
-
255
254
  test "#<=> compares regardless of the unit" do
256
255
  assert_equal (-1), @magic <=> Magic.new(20, :fire)
257
256
  assert_equal 1, @magic <=> Magic.new(9, :magic_missile)
data/test/unit_test.rb CHANGED
@@ -43,6 +43,10 @@ class Measured::UnitTest < ActiveSupport::TestCase
43
43
  assert_equal "pie (1/2 sweet)", Measured::Unit.new(:pie, aliases: ["cake"], value: "0.5 sweet").to_s
44
44
  end
45
45
 
46
+ test "#to_s can drop the conversion amount" do
47
+ assert_equal "pie", Measured::Unit.new(:pie).to_s(with_conversion_string: false)
48
+ end
49
+
46
50
  test "#inspect returns an expected string" do
47
51
  assert_equal "#<Measured::Unit: pie>", Measured::Unit.new(:pie).inspect
48
52
  assert_equal "#<Measured::Unit: pie (cake, semi-sweet)>", Measured::Unit.new(:pie, aliases: ["cake", "semi-sweet"]).inspect
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.6.0
4
+ version: 2.8.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: 2020-12-10 00:00:00.000000000 Z
13
+ date: 2021-12-07 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -126,6 +126,7 @@ 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
130
131
  - lib/measured/missing_conversion_path.rb
131
132
  - lib/measured/parser.rb
@@ -179,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
180
  - !ruby/object:Gem::Version
180
181
  version: '0'
181
182
  requirements: []
182
- rubygems_version: 3.0.3
183
+ rubygems_version: 3.2.20
183
184
  signing_key:
184
185
  specification_version: 4
185
186
  summary: Encapsulate measurements with their units in Ruby