unitsml 0.6.10 → 0.6.11

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.
data/lib/unitsml/opal.rb CHANGED
@@ -37,6 +37,7 @@ require "unitsml/errors/invalid_model_error"
37
37
  require "unitsml/errors/opal_payload_not_bundled_error"
38
38
  require "unitsml/errors/plurimath_load_error"
39
39
  require "unitsml/errors/parg_load_error"
40
+ require "unitsml/errors/quantity_errors"
40
41
  require "unitsml/errors/unsupported_payload_type_error"
41
42
 
42
43
  require "unitsml/opal/database_payload"
@@ -92,6 +93,7 @@ require "unitsml/mathml_helper"
92
93
  require "unitsml/number"
93
94
  require "unitsml/parg"
94
95
  require "unitsml/parser"
96
+ require "unitsml/quantity"
95
97
  require "unitsml/prefix"
96
98
  require "unitsml/prefix_adapter"
97
99
  require "unitsml/sqrt"
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsml
4
+ # A quantity: a magnitude plus a unit identity, with unit-aware
5
+ # arithmetic (unitsml-ruby#82 R-A1/A2/A4-skeleton/A5).
6
+ #
7
+ # Dimensions come from the parsed unit expression via UnitsDB; every
8
+ # operation checks them. Scale legality (R-A4) gates multiplication
9
+ # and division on non-ratio scales; the classification is pluggable
10
+ # per quantity until the unitsdb scale-legality data lands (#130).
11
+ class Quantity
12
+ attr_reader :value, :formula, :scale
13
+
14
+ def initialize(value:, unit_expression: nil, scale: :ratio)
15
+ @value = value
16
+ @formula = unit_expression && Unitsml.parse(unit_expression)
17
+ @scale = scale.to_sym
18
+ end
19
+
20
+ # Canonical dimension identity; "1" (dimensionless) for pure numbers.
21
+ def dimension_id
22
+ @dimension_id ||= begin
23
+ list = dimension_list
24
+ list && (Utility.dim_id(list) || "1")
25
+ end
26
+ end
27
+
28
+ # Base-dimension exponents summed across the composed expression —
29
+ # V (L²MT⁻³I⁻¹) times A (I¹) merges to the watt vector L²MT⁻³.
30
+ def dimension_vector
31
+ vector = raw_dimension_list.each_with_object(Hash.new(0)) do |entry, acc|
32
+ exponent = entry[:exponent]
33
+ exponent = exponent.raw_value if exponent.respond_to?(:raw_value)
34
+ acc[entry[:dimension]] += exponent.to_i
35
+ end
36
+ vector.delete_if { |_, exponent| exponent.zero? }
37
+ vector
38
+ end
39
+
40
+ # Normalized unit expression text; the value's to_s for pure numbers.
41
+ def norm_text
42
+ formula&.norm_text || value.to_s
43
+ end
44
+
45
+ def *(other)
46
+ if other.is_a?(Numeric)
47
+ gate_scaling!
48
+ return Quantity.new(value: value * other, unit_expression: norm_text)
49
+ end
50
+
51
+ scale_ratio_gate!(other, "multiplication")
52
+ Quantity.new(value: value * other.value,
53
+ unit_expression: compose(self, other, "*"))
54
+ end
55
+
56
+ # rubocop:disable Metrics/AbcSize
57
+ def /(other)
58
+ if other.is_a?(Numeric)
59
+ gate_zero!(other)
60
+ gate_scaling!
61
+ return Quantity.new(value: value / other, unit_expression: norm_text)
62
+ end
63
+
64
+ scale_ratio_gate!(other, "division")
65
+ if other.value.zero?
66
+ raise Errors::IndeterminateResultError,
67
+ "division by a zero quantity is indeterminate"
68
+ end
69
+
70
+ Quantity.new(value: value / other.value,
71
+ unit_expression: compose(self, other, "/"))
72
+ end
73
+ # rubocop:enable Metrics/AbcSize
74
+
75
+ # R-A4: differences are legal on both ratio and interval scales;
76
+ # only the dimension identity must match.
77
+ def +(other)
78
+ quantity_kind!(other)
79
+ gate_dimension!(other)
80
+ Quantity.new(value: value + other.value, unit_expression: norm_text)
81
+ end
82
+
83
+ def -(other)
84
+ quantity_kind!(other)
85
+ gate_dimension!(other)
86
+ Quantity.new(value: value - other.value, unit_expression: norm_text)
87
+ end
88
+
89
+ def ==(other)
90
+ other.is_a?(Quantity) && value == other.value &&
91
+ dimension_vector == other.dimension_vector
92
+ end
93
+
94
+ private
95
+
96
+ # Base dimensions in canonical DIMS_VECTOR order; nil when all
97
+ # exponents cancel (dimensionless).
98
+ def dimension_list
99
+ merged = Utility::DIMS_VECTOR.filter_map do |dimension|
100
+ exponent = dimension_vector[dimension]
101
+ next if exponent.zero?
102
+
103
+ { dimension: dimension, exponent: exponent }
104
+ end
105
+ merged.empty? ? nil : merged
106
+ end
107
+
108
+ def raw_dimension_list
109
+ return [] unless formula
110
+
111
+ Utility.units2dimensions(extract_units(formula.value))
112
+ end
113
+
114
+ # Formula#units renders XML; the dimension identity needs the raw
115
+ # decomposition, so extract it the same way Formula does internally.
116
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
117
+ def extract_units(formula)
118
+ formula.each_with_object([]) do |term, units_arr|
119
+ case term
120
+ when Unit
121
+ units_arr << term.dup
122
+ when Formula
123
+ units_arr.concat(extract_units(term.value.dup))
124
+ when Sqrt
125
+ next unless term.value.is_a?(Unit)
126
+
127
+ sqrt_unit = term.value.dup
128
+ sqrt_unit.power_numerator = Number.new("0.5")
129
+ units_arr << sqrt_unit
130
+ when Fenced
131
+ units_arr.concat(extract_units(term.value.value.dup))
132
+ end
133
+ end
134
+ end
135
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
136
+
137
+ # R-A4: ratio-scale operations are specified errors on non-ratio
138
+ # quantities. Differences and comparisons remain legal on both.
139
+ # Scalar multiplication/division scales the magnitude: legal on
140
+ # ratio scales, a specified error on non-ratio ones.
141
+ def gate_scaling!
142
+ return if scale == :ratio
143
+
144
+ raise Errors::ScaleOperationError,
145
+ "scaling is not legal on a #{scale} scale quantity"
146
+ end
147
+
148
+ def scale_ratio_gate!(other, operation)
149
+ return if scale == :ratio && other.scale == :ratio
150
+
151
+ raise Errors::ScaleOperationError,
152
+ "#{operation} is not legal between #{scale} and " \
153
+ "#{other.scale} scale quantities"
154
+ end
155
+
156
+ def quantity_kind!(other)
157
+ unless other.is_a?(Quantity)
158
+ raise ArgumentError, "expected a Unitsml::Quantity, got #{other.class}"
159
+ end
160
+ end
161
+
162
+ def gate_dimension!(other)
163
+ return if dimension_vector == other.dimension_vector
164
+
165
+ raise Errors::DimensionMismatchError,
166
+ "dimension mismatch: #{dimension_vector.inspect} " \
167
+ "vs #{other.dimension_vector.inspect}"
168
+ end
169
+
170
+ def gate_zero!(other)
171
+ return unless other.zero?
172
+
173
+ raise Errors::IndeterminateResultError,
174
+ "division by a zero quantity is indeterminate"
175
+ end
176
+
177
+ def compose(left, right, operator)
178
+ "#{left.norm_text}#{operator}#{right.norm_text}"
179
+ end
180
+ end
181
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unitsml
4
- VERSION = "0.6.10"
4
+ VERSION = "0.6.11"
5
5
  end
data/lib/unitsml.rb CHANGED
@@ -26,6 +26,7 @@ module Unitsml
26
26
  autoload :Unitsdb, "unitsml/unitsdb"
27
27
  autoload :Utility, "unitsml/utility"
28
28
  autoload :VERSION, "unitsml/version"
29
+ autoload :Quantity, "unitsml/quantity"
29
30
  autoload :Xml, "unitsml/xml"
30
31
 
31
32
  def parse(string)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unitsml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.10
4
+ version: 0.6.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -115,6 +115,7 @@ files:
115
115
  - lib/unitsml/errors/opal_payload_not_bundled_error.rb
116
116
  - lib/unitsml/errors/parg_load_error.rb
117
117
  - lib/unitsml/errors/plurimath_load_error.rb
118
+ - lib/unitsml/errors/quantity_errors.rb
118
119
  - lib/unitsml/errors/unsupported_payload_type_error.rb
119
120
  - lib/unitsml/extender.rb
120
121
  - lib/unitsml/fenced.rb
@@ -157,6 +158,7 @@ files:
157
158
  - lib/unitsml/parser.rb
158
159
  - lib/unitsml/prefix.rb
159
160
  - lib/unitsml/prefix_adapter.rb
161
+ - lib/unitsml/quantity.rb
160
162
  - lib/unitsml/sqrt.rb
161
163
  - lib/unitsml/unit.rb
162
164
  - lib/unitsml/unitsdb.rb