unitsml 0.2.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.
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ox"
4
+ module Unitsml
5
+ module Utility
6
+
7
+ UNITSML_NS = "https://schema.unitsml.org/unitsml/1.0".freeze
8
+ # Unit to dimension
9
+ U2D = {
10
+ "m" => { dimension: "Length", order: 1, symbol: "L" },
11
+ "g" => { dimension: "Mass", order: 2, symbol: "M" },
12
+ "kg" => { dimension: "Mass", order: 2, symbol: "M" },
13
+ "s" => { dimension: "Time", order: 3, symbol: "T" },
14
+ "A" => { dimension: "ElectricCurrent", order: 4, symbol: "I" },
15
+ "K" => { dimension: "ThermodynamicTemperature", order: 5,
16
+ symbol: "Theta" },
17
+ "degK" => { dimension: "ThermodynamicTemperature", order: 5,
18
+ symbol: "Theta" },
19
+ "mol" => { dimension: "AmountOfSubstance", order: 6, symbol: "N" },
20
+ "cd" => { dimension: "LuminousIntensity", order: 7, symbol: "J" },
21
+ "deg" => { dimension: "PlaneAngle", order: 8, symbol: "phi" },
22
+ }.freeze
23
+ # Dimesion for dim_(dimesion) input
24
+ Dim2D = {
25
+ "dim_L" => U2D["m"],
26
+ "dim_M" => U2D["g"],
27
+ "dim_T" => U2D["s"],
28
+ "dim_I" => U2D["A"],
29
+ "dim_Theta" => U2D["K"],
30
+ "dim_N" => U2D["mol"],
31
+ "dim_J" => U2D["cd"],
32
+ "dim_phi" => U2D["deg"],
33
+ }.freeze
34
+ DIMS_VECTOR = %w[
35
+ ThermodynamicTemperature
36
+ AmountOfSubstance
37
+ LuminousIntensity
38
+ ElectricCurrent
39
+ PlaneAngle
40
+ Length
41
+ Mass
42
+ Time
43
+ ].freeze
44
+
45
+ class << self
46
+ include Unitsml::Unitsdb
47
+
48
+ def fields(unit)
49
+ Unitsdb.units.dig(unit, :fields)
50
+ end
51
+
52
+ def units2dimensions(units)
53
+ norm = decompose_units_list(units)
54
+ return if norm.any? { |u| u.nil? || u[:unit].unit_name == "unknown" || u[:prefix] == "unknown" }
55
+
56
+ norm.map do |u|
57
+ unit_name = u[:unit].unit_name
58
+ {
59
+ dimension: U2D[unit_name][:dimension],
60
+ unit: unit_name,
61
+ exponent: u[:unit].power_numerator || 1,
62
+ symbol: U2D[unit_name][:symbol],
63
+ }
64
+ end.sort { |a, b| U2D[a[:unit]][:order] <=> U2D[b[:unit]][:order] }
65
+ end
66
+
67
+ def dim_id(dims)
68
+ return nil if dims.nil? || dims.empty?
69
+
70
+ dimensions = Unitsdb.dimensions_hash.values
71
+ dim_hash = dims.each_with_object({}) { |h, m| m[h[:dimension]] = h }
72
+ dims_vector = DIMS_VECTOR.map { |h| dim_hash.dig(h, :exponent) }.join(":")
73
+ id = dimensions.select { |d| d[:vector] == dims_vector }&.first&.dig(:id) and return id.to_s
74
+ "D_" + dims.map do |d|
75
+ (U2D.dig(d[:unit], :symbol) || Dim2D.dig(d[:id], :symbol)) +
76
+ (d[:exponent] == 1 ? "" : float_to_display(d[:exponent]))
77
+ end.join("")
78
+ end
79
+
80
+ def decompose_units_list(units)
81
+ gather_units(units.map { |u| decompose_unit(u) }.flatten)
82
+ end
83
+
84
+ def decompose_unit(u)
85
+ if u&.unit_name == "g" || u.system_type == "SI_base"
86
+ { unit: u, prefix: u&.prefix }
87
+ elsif !u.si_derived_bases
88
+ { unit: Unit.new("unknown") }
89
+ else
90
+ u.si_derived_bases.each_with_object([]) do |k, m|
91
+ prefix = if !k["prefix"].nil? && !k["prefix"].empty?
92
+ combine_prefixes(prefix_object(k["prefix"]), u.prefix)
93
+ else
94
+ u.prefix
95
+ end
96
+ unit_name = Unitsdb.load_units.dig(k.dig("id"), "unit_symbols", 0, "id")
97
+ exponent = (k["power"]&.to_i || 1) * (u.power_numerator&.to_f || 1)
98
+ m << { prefix: prefix,
99
+ unit: Unit.new(unit_name, exponent, prefix: prefix),
100
+ }
101
+ end
102
+ end
103
+ end
104
+
105
+ def gather_units(units)
106
+ units.sort_by { |a| a[:unit]&.unit_name }.each_with_object([]) do |k, m|
107
+ if m.empty? || m[-1][:unit]&.unit_name != k[:unit]&.unit_name
108
+ m << k
109
+ else
110
+ m[-1][:unit]&.power_numerator = (k[:unit]&.power_numerator&.to_f || 1) + (m[-1][:unit]&.power_numerator&.to_f || 1)
111
+ m[-1] = {
112
+ prefix: combine_prefixes(prefix_object(m[-1][:prefix]), prefix_object(k[:prefix])),
113
+ unit: m[-1][:unit],
114
+ }
115
+ end
116
+ end
117
+ end
118
+
119
+ def prefix_object(prefix)
120
+ return prefix unless prefix.is_a?(String)
121
+ return nil unless Unitsdb.prefixes.any?(prefix)
122
+
123
+ prefix.is_a?(String) ? Prefix.new(prefix) : prefix
124
+ end
125
+
126
+ def combine_prefixes(p1, p2)
127
+ return nil if p1.nil? && p2.nil?
128
+ return p1.symbolid if p2.nil?
129
+ return p2.symbolid if p1.nil?
130
+ return "unknown" if p1.base != p2.base
131
+
132
+ Unitsdb.prefixes_hash.each do |prefix_name, _|
133
+ p = prefix_object(prefix_name)
134
+ return p if p.base == p1.base && p.power == p1.power + p2.power
135
+ end
136
+
137
+ "unknown"
138
+ end
139
+
140
+ def unit(units, formula, dims, norm_text, name)
141
+ attributes = { xmlns: UNITSML_NS, "xml:id": unit_id(norm_text) }
142
+ attributes[:dimensionURL] = "##{dim_id(dims)}" if dims
143
+ unit_node = ox_element("Unit", attributes: attributes)
144
+ nodes = Array(unitsystem(units))
145
+ nodes += Array(unitname(units, norm_text, name))
146
+ nodes += Array(unitsymbols(formula))
147
+ nodes += Array(rootunits(units))
148
+ Ox.dump(update_nodes(unit_node, nodes))
149
+ .gsub("&lt;", "<")
150
+ .gsub("&gt;", ">")
151
+ .gsub("&amp;", "&")
152
+ .gsub(/−/, "&#x2212;")
153
+ .gsub(/⋅/, "&#x22c5;")
154
+ end
155
+
156
+ def unitname(units, text, name)
157
+ name ||= Unitsdb.units[text] ? Unit.new(text).enumerated_name : text
158
+ ox_element("UnitName", attributes: { "xml:lang": "en" }) << name
159
+ end
160
+
161
+ def postprocess_normtext(units)
162
+ units.map { |u| "#{u.prefix_name}#{u.unit_name}#{display_exp(u)}" }.join("*")
163
+ end
164
+
165
+ def display_exp(unit)
166
+ unit.power_numerator && unit.power_numerator != "1" ? "^#{unit.power_numerator}" : ""
167
+ end
168
+
169
+ def unitsymbols(formula)
170
+ [
171
+ (ox_element("UnitSymbol", attributes: { type: "HTML" }) << formula.to_html),
172
+ (ox_element("UnitSymbol", attributes: { type: "MathMl" }) << Ox.parse(formula.to_mathml)),
173
+ ]
174
+ end
175
+
176
+ def unitsystem(units)
177
+ ret = []
178
+ if units.any? { |u| u.system_name != "SI" }
179
+ ret << ox_element("UnitSystem", attributes: { name: "not_SI", type: "not_SI", "xml:lang": 'en-US' })
180
+ end
181
+ if units.any? { |u| u.system_name == "SI" }
182
+ if units.size == 1
183
+ base = units[0].system_type == "SI-base"
184
+ base = true if units[0].unit_name == "g" && units[0]&.prefix_name == "k"
185
+ end
186
+ ret << ox_element("UnitSystem", attributes: { name: "SI", type: (base ? 'SI_base' : 'SI_derived'), "xml:lang": 'en-US' })
187
+ end
188
+ ret
189
+ end
190
+
191
+ def dimension(norm_text)
192
+ return unless fields(norm_text)&.dig("dimension_url")
193
+
194
+ dim_id = fields(norm_text).dig("dimension_url").sub("#", '')
195
+ dim_node = ox_element("Dimension", attributes: { xmlns: UNITSML_NS, "xml:id": dim_id })
196
+ Ox.dump(
197
+ update_nodes(
198
+ dim_node,
199
+ dimid2dimensions(dim_id)&.compact&.map { |u| dimension1(u) }
200
+ )
201
+ )
202
+ end
203
+
204
+ def dimension1(dim)
205
+ attributes = {
206
+ symbol: dim[:symbol],
207
+ powerNumerator: float_to_display(dim[:exponent])
208
+ }
209
+ ox_element(dim[:dimension], attributes: attributes)
210
+ end
211
+
212
+ def float_to_display(float)
213
+ float.to_f.round(1).to_s.sub(/\.0$/, "")
214
+ end
215
+
216
+ def dimid2dimensions(normtext)
217
+ dims ||= Unitsdb.load_dimensions[normtext]
218
+ dims&.keys&.reject { |d| d.is_a?(Symbol) }&.map do |k|
219
+ humanized = k.split("_").map(&:capitalize).join
220
+ next unless DIMS_VECTOR.include?(humanized)
221
+
222
+ {
223
+ dimension: humanized,
224
+ symbol: dims.dig(k, "symbol"),
225
+ exponent: dims.dig(k, "powerNumerator")
226
+ }
227
+ end
228
+ end
229
+
230
+ def prefixes(units)
231
+ uniq_prefixes = units.map { |unit| unit.prefix }.compact.uniq {|d| d.prefix_name }
232
+ uniq_prefixes.map do |p|
233
+ prefix_attr = { xmlns: UNITSML_NS, prefixBase: p&.base, prefixPower: p&.power, "xml:id": p&.id }
234
+ prefix_node = ox_element("Prefix", attributes: prefix_attr)
235
+ contents = []
236
+ contents << (ox_element("PrefixName", attributes: { "xml:lang": "en" }) << p&.name)
237
+ contents << (ox_element("PrefixSymbol", attributes: { type: "ASCII" }) << p&.to_asciimath)
238
+ contents << (ox_element("PrefixSymbol", attributes: { type: "unicode" }) << p&.to_unicode)
239
+ contents << (ox_element("PrefixSymbol", attributes: { type: "LaTex" }) << p&.to_latex)
240
+ contents << (ox_element("PrefixSymbol", attributes: { type: "HTML" }) << p&.to_html)
241
+ Ox.dump(update_nodes(prefix_node, contents)).gsub("&amp;", "&")
242
+ end.join("\n")
243
+ end
244
+
245
+ def rootunits(units)
246
+ return if units.size == 1 && !units[0].prefix
247
+
248
+ root_unit = ox_element("RootUnits")
249
+ units.each do |u|
250
+ attributes = { unit: u.enumerated_name }
251
+ attributes[:prefix] = u.prefix_name if u.prefix
252
+ u.power_numerator && u.power_numerator != "1" and
253
+ attributes[:powerNumerator] = u.power_numerator
254
+ root_unit << ox_element("EnumeratedRootUnit", attributes: attributes)
255
+ end
256
+ root_unit
257
+ end
258
+
259
+ def unit_id(text)
260
+ norm_text = text
261
+ text = text&.gsub(/[()]/, "")
262
+ /-$/.match(text) and return Unitsdb.prefixes[text.sub(/-$/, "")][:id]
263
+ unit_hash = Unitsdb.units[norm_text]
264
+ "U_#{unit_hash ? unit_hash[:id]&.gsub(/'/, '_') : norm_text&.gsub(/\*/, '.')&.gsub(/\^/, '')}"
265
+ end
266
+
267
+ def dimension_components(dims)
268
+ return if dims.nil? || dims.empty?
269
+
270
+ attributes = { xmlns: UNITSML_NS, "xml:id": dim_id(dims) }
271
+ dim_node = ox_element("Dimension", attributes: attributes)
272
+ Ox.dump(update_nodes(dim_node, dims.map { |u| dimension1(u) }))
273
+ end
274
+
275
+ def quantity(normtext, quantity)
276
+ units = Unitsdb.units
277
+ quantity_references = units.dig(normtext, :fields, "quantity_reference")
278
+ return unless units[normtext] && quantity_references.size == 1 ||
279
+ Unitsdb.quantities[quantity]
280
+
281
+ id = quantity || quantity_references&.first&.dig("url")
282
+ attributes = { xmlns: UNITSML_NS, "xml:id": id.sub('#', '') }
283
+ dim_url = units.dig(normtext, :fields, "dimension_url")
284
+ dim_url and attributes[:dimensionURL] = "#{dim_url}"
285
+ attributes[:quantityType] = "base"
286
+ quantity_element = ox_element("Quantity", attributes: attributes)
287
+ Ox.dump(update_nodes(quantity_element, quantity_name(id.sub('#', ''))))
288
+ end
289
+
290
+ def quantity_name(id)
291
+ ret = []
292
+ Unitsdb.quantities[id]&.dig("quantity_name")&.each do |q|
293
+ node = (ox_element("QuantityName", attributes: { "xml:lang": "en-US" }) << q)
294
+ ret << node
295
+ end
296
+ ret
297
+ end
298
+
299
+ def ox_element(node, attributes: [])
300
+ element = Ox::Element.new(node)
301
+ attributes&.each { |attr_key, attr_value| element[attr_key] = attr_value }
302
+ element
303
+ end
304
+
305
+ def update_nodes(element, nodes)
306
+ nodes&.each { |node| element << node unless node.nil? }
307
+ element
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,3 @@
1
+ module Unitsml
2
+ VERSION = "0.2.0"
3
+ end
data/lib/unitsml.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "unitsml/sqrt"
2
+ require "unitsml/unit"
3
+ require "unitsml/parse"
4
+ require "unitsml/parser"
5
+ require "unitsml/prefix"
6
+ require "unitsml/formula"
7
+ require "unitsml/version"
8
+ require "unitsml/unitsdb"
9
+ require "unitsml/extender"
10
+ require "unitsml/dimension"
11
+ require "unitsml/transform"
12
+
13
+ module Unitsml
14
+ def self.parse(string)
15
+ Unitsml::Parser.new(string).parse
16
+ end
17
+ end