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.
- checksums.yaml +4 -4
- data/lib/unitsml/errors/quantity_errors.rb +10 -0
- data/lib/unitsml/errors.rb +4 -0
- data/lib/unitsml/formula.rb +1 -1
- data/lib/unitsml/opal/database_payload.rb +1 -1
- data/lib/unitsml/opal.rb +2 -0
- data/lib/unitsml/quantity.rb +181 -0
- data/lib/unitsml/version.rb +1 -1
- data/lib/unitsml.rb +1 -0
- metadata +3 -1
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
|
data/lib/unitsml/version.rb
CHANGED
data/lib/unitsml.rb
CHANGED
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.
|
|
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
|