unitsml 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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