Class: Asciimath2UnitsML::Conv

Inherits:
Object
  • Object
show all
Includes:
Rsec::Helpers
Defined in:
lib/asciimath2unitsml/conv.rb,
lib/asciimath2unitsml/unit.rb,
lib/asciimath2unitsml/parse.rb,
lib/asciimath2unitsml/render.rb

Constant Summary collapse

U2D =
{
  "m" => { dimension: "Length", order: 1, symbol: "L" },
  "g" => { dimension: "Mass", order: 2, symbol: "M" },
  "kg" => { dimension: "Mass", order: 2, symbol: "M" },
  "s" => { dimension: "Time", order: 3, symbol: "T" },
  "A" => { dimension: "ElectricCurrent", order: 4, symbol: "I" },
  "K" => { dimension: "ThermodynamicTemperature", order: 5, symbol: "Theta" },
  "degK" => { dimension: "ThermodynamicTemperature", order: 5, symbol: "Theta" },
  "mol" => { dimension: "AmountOfSubstance", order: 6, symbol: "N" },
  "cd" => { dimension: "LuminousIntensity", order: 7, symbol: "J" },
  "deg" => { dimension: "PlaneAngle", order: 8, symbol: "Phi" },
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Conv

Returns a new instance of Conv.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/asciimath2unitsml/conv.rb', line 16

def initialize(options = {})
  @dimensions_id = read_yaml("../unitsdb/dimensions.yaml").each_with_object({}) do |(k, v), m|
    m[k.to_s] = UnitsDB::Dimension.new(k, v)
  end
  @prefixes_id = read_yaml("../unitsdb/prefixes.yaml").each_with_object({}) do |(k, v), m|
    m[k] = UnitsDB::Prefix.new(k, v)
  end
  @prefixes = flip_name_and_symbol(@prefixes_id)
  @quantities = read_yaml("../unitsdb/quantities.yaml").each_with_object({}) do |(k, v), m|
    m[k.to_s] = UnitsDB::Quantity.new(k, v)
  end
  @units_id = read_yaml("../unitsdb/units.yaml").each_with_object({}) do |(k, v), m|
    m[k.to_s] = UnitsDB::Unit.new(k.to_s, v)
  end
  @units = flip_name_and_symbols(@units_id)
  @symbols = @units.each_with_object({}) do |(k, v), m|
    v.symbolids.each { |x| m[x] = v.symbols_hash[x] }
  end
  @parser = parser
  @multiplier = multiplier(options[:multiplier] || "\u00b7")
end

Instance Method Details

#ambig_unitsObject



196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/asciimath2unitsml/parse.rb', line 196

def ambig_units
  u = @units_id.each_with_object({}) do |(k, v), m|
    v.symbolids.each do |x|
      next if %r{[*/^]}.match(x)
      next unless v.symbols_hash[x][:html] != x
      m[v.symbols_hash[x][:html]] ||= []
      m[v.symbols_hash[x][:html]] << x
    end
  end
  u.keys.each { |k| u[k] = u[k].unshift(k) if @symbols.dig(k, :html) == k }
  render_ambig_units(u)
end

#asciimath2mathml(expression) ⇒ Object



184
185
186
187
188
# File 'lib/asciimath2unitsml/parse.rb', line 184

def asciimath2mathml(expression)
  AsciiMath::MathMLBuilder.new(:msword => true).append_expression(
    AsciiMath.parse(HTMLEntities.new.decode(expression)).ast).to_s.
  gsub(/<math>/, "<math xmlns='#{MATHML_NS}'>")
end

#Asciimath2UnitsML(expression) ⇒ Object



146
147
148
149
# File 'lib/asciimath2unitsml/parse.rb', line 146

def Asciimath2UnitsML(expression)
  xml = Nokogiri::XML(asciimath2mathml(expression))
  MathML2UnitsML(xml).to_xml
end

#combine_prefixes(p1, p2) ⇒ Object



139
140
141
142
143
144
145
146
147
148
# File 'lib/asciimath2unitsml/conv.rb', line 139

def combine_prefixes(p1, p2)
  return nil if p1.nil? && p2.nil?
  return p1.symbolid if p2.nil?
  return p2.symbolid if p1.nil?
  return "unknown" if p1.base != p2.base
  @prefixes.each do |_, p|
    return p.symbolid if p.base == p1.base && p.power == p1.power + p2.power
  end
  "unknown"
end

#compose_name(units, text) ⇒ Object

TODO: compose name from the component units



57
58
59
# File 'lib/asciimath2unitsml/unit.rb', line 57

def compose_name(units, text)
  text
end

#decompose_unit(u) ⇒ Object

treat g not kg as base unit: we have stripped the prefix k in parsing reduce units down to basic units



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/asciimath2unitsml/conv.rb', line 123

def decompose_unit(u)
  if u[:unit].nil? then u
  elsif u[:unit] == "g" then u
  elsif @units[u[:unit]].system_type == "SI_base" then u
  elsif !@units[u[:unit]].si_derived_bases
    { prefix: u[:prefix], unit: "unknown", exponent: u[:exponent] }
  else
    @units[u[:unit]].si_derived_bases.each_with_object([]) do |k, m|
      m << { prefix: !k[:prefix].nil? && !k[:prefix].empty? ? 
             combine_prefixes(@prefixes_id[k[:prefix]], @prefixes[u[:prefix]]) : u[:prefix],
             unit: @units_id[k[:id]].symbolid,
             exponent: (k[:power]&.to_i || 1) * (u[:exponent]&.to_f || 1) }
    end
  end
end

#decompose_units(units) ⇒ Object



106
107
108
# File 'lib/asciimath2unitsml/conv.rb', line 106

def decompose_units(units)
  gather_units(units_only(units).map { |u| decompose_unit(u) }.flatten)
end

#dedup_ids(xml) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
# File 'lib/asciimath2unitsml/parse.rb', line 172

def dedup_ids(xml)
  %w(Unit Dimension Prefix Quantity).each do |t|
    xml.xpath(".//m:#{t}/@xml:id", "m" => UNITSML_NS).map { |a| a.text }.uniq.each do |v|
      xml.xpath(".//*[@xml:id = '#{v}']").each_with_index do |n, i|
        next if i == 0
        n.remove
      end
    end
  end
  xml
end

#delimspace(rendering, elem) ⇒ Object



165
166
167
168
169
170
# File 'lib/asciimath2unitsml/parse.rb', line 165

def delimspace(rendering, elem)
  return "" if elem&.previous_element && elem&.previous_element.name != "mn"
  text = HTMLEntities.new.encode(Nokogiri::XML("<mrow>#{rendering}</mrow>").text.strip)
  /\p{L}|\p{N}/.match(text) ?
    "<mo rspace='thickmathspace'>&#x2062;</mo>" : "<mo>&#x2062;</mo>"
end

#dim_id(dims) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/asciimath2unitsml/conv.rb', line 94

def dim_id(dims)
  return nil if dims.nil? || dims.empty?
  dimhash = dims.each_with_object({}) { |h, m| m[h[:dimension]] = h }
  dimsvector = %w(Length Mass Time ElectricCurrent ThermodynamicTemperature 
                  AmountOfSubstance LuminousIntensity PlaneAngle)
    .map { |h| dimhash.dig(h, :exponent) }.join(":")
  id = @dimensions_id&.values&.select { |d| d.vector == dimsvector }&.first&.id and return id.to_s
  "D_" + dims.map do |d|
    U2D[d[:unit]][:symbol] + (d[:exponent] == 1 ? "" : float_to_display(d[:exponent]))
  end.join("")
end

#dimension(normtext) ⇒ Object



177
178
179
180
181
182
183
184
185
# File 'lib/asciimath2unitsml/conv.rb', line 177

def dimension(normtext)
  return unless @units[normtext]&.dimension 
  dims = dimid2dimensions(@units[normtext]&.dimension)
  <<~END
  <Dimension xmlns='#{UNITSML_NS}' xml:id="#{@units[normtext]&.dimension}">
  #{dims.map { |u| dimension1(u) }.join("\n") }
  </Dimension>
  END
end

#dimension1(u) ⇒ Object



90
91
92
# File 'lib/asciimath2unitsml/conv.rb', line 90

def dimension1(u)
  %(<#{u[:dimension]} symbol="#{u[:symbol]}" powerNumerator="#{float_to_display(u[:exponent])}"/>)
end

#dimension_components(dims) ⇒ Object



57
58
59
60
61
62
63
64
# File 'lib/asciimath2unitsml/conv.rb', line 57

def dimension_components(dims)
  return if dims.nil? || dims.empty?
  <<~END
  <Dimension xmlns='#{UNITSML_NS}' xml:id="#{dim_id(dims)}">
  #{dims.map { |u| dimension1(u) }.join("\n") }
  </Dimension>
  END
end

#dimid2dimensions(normtext) ⇒ Object



169
170
171
172
173
174
175
# File 'lib/asciimath2unitsml/conv.rb', line 169

def dimid2dimensions(normtext)
  @dimensions_id[normtext].keys.map do |k|
    { dimension: k,
      symbol: U2D.values.select { |v| v[:dimension] == k }.first[:symbol],
      exponent: @dimensions_id[normtext].exponent(k) }
  end
end

#embeddedmathml(mathml) ⇒ Object



190
191
192
193
194
# File 'lib/asciimath2unitsml/parse.rb', line 190

def embeddedmathml(mathml)
  x = Nokogiri::XML(mathml)
  x.xpath(".//m:mi", "m" => MATHML_NS).each { |mi| mi["mathvariant"] = "normal" }
  x.children.to_xml
end

#flip_name_and_symbol(hash) ⇒ Object



9
10
11
12
13
14
# File 'lib/asciimath2unitsml/parse.rb', line 9

def flip_name_and_symbol(hash)
  hash.each_with_object({}) do |(k, v), m|
    next if v.name.nil? || v.name.empty?
    m[v.symbolid] = v
  end
end

#flip_name_and_symbols(hash) ⇒ Object



16
17
18
19
20
21
# File 'lib/asciimath2unitsml/parse.rb', line 16

def flip_name_and_symbols(hash)
  hash.each_with_object({}) do |(k, v), m|
    next if v.name.nil? || v.name.empty?
    v.symbolids.each { |s| m[s] = v }
  end
end

#float_to_display(f) ⇒ Object



38
39
40
# File 'lib/asciimath2unitsml/conv.rb', line 38

def float_to_display(f)
  ret = f.to_f.round(1).to_s.sub(/\.0$/, "")
end

#gather_units(units) ⇒ Object



110
111
112
113
114
115
116
117
118
119
# File 'lib/asciimath2unitsml/conv.rb', line 110

def gather_units(units)
  units.sort { |a, b| a[:unit] <=> b[:unit] }.each_with_object([]) do |k, m|
    if m.empty? || m[-1][:unit] != k[:unit] then m << k
    else
      m[-1] = { prefix: combine_prefixes(@prefixes[m[-1][:prefix]], @prefixes[k[:prefix]]),
                unit: m[-1][:unit],
                exponent: (k[:exponent]&.to_f || 1) + (m[-1][:exponent]&.to_f || 1) }
    end
  end
end

#html2adoc(k) ⇒ Object



222
223
224
225
226
# File 'lib/asciimath2unitsml/parse.rb', line 222

def html2adoc(k)
  k.gsub(%r{<i>}, "__").gsub(%r{</i>}, "__")
    .gsub(%r{<sup>}, "^").gsub(%r{</sup>}, "^")
    .gsub(%r{<sub>}, "~").gsub(%r{</sub>}, "~")
end

#htmlent(x) ⇒ Object



18
19
20
21
# File 'lib/asciimath2unitsml/render.rb', line 18

def htmlent(x)
  HTMLEntities.new.decode(x).split(/([<>&])/)
    .map { |c| /[<>'"]/.match(c) ? c : HTMLEntities.new.encode(c, :hexadecimal) }.join
end

#htmlsymbol(units, normalise) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
# File 'lib/asciimath2unitsml/render.rb', line 23

def htmlsymbol(units, normalise)
  units.map do |u|
    if u[:multiplier] then u[:multiplier] == "*" ? @multiplier[:html] : u[:multiplier]
    elsif u[:unit].nil? && u[:prefix]
      @prefixes[u[:prefix]].html
    else
      base = (u[:prefix] || "") + render(normalise ? @units[u[:unit]].symbolid : u[:unit], :html)
      htmlsymbol_exponent(u, base)
    end
  end.join("")
end

#htmlsymbol_exponent(u, base) ⇒ Object



35
36
37
38
39
40
41
42
43
# File 'lib/asciimath2unitsml/render.rb', line 35

def htmlsymbol_exponent(u, base)
  if u[:display_exponent] == "0.5"
    base = "&#x221a;#{base}"
  elsif u[:display_exponent]
    exp = "<sup>#{u[:display_exponent].sub(/-/, "&#x2212;")}</sup>"
    base += exp
  end
  base
end

#MathML2UnitsML(xml) ⇒ Object

www.w3.org/TR/mathml-units/ section 2: delimit number Invisible-Times unit



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/asciimath2unitsml/parse.rb', line 152

def MathML2UnitsML(xml)
  xml.is_a? String and xml = Nokogiri::XML(xml)
  xml.xpath(".//m:mtext", "m" => MATHML_NS).each do |x|
    next unless %r{^unitsml\(.+\)$}.match(x.text)
    text = x.text.sub(%r{^unitsml\((.+)\)$}m, "\\1")
    units, origtext, normtext, quantity, name, symbol = parse(text)
    rendering = symbol ? embeddedmathml(asciimath2mathml(symbol)) : mathmlsymbol(units, false)
    x.replace("#{delimspace(rendering, x)}<mrow xref='#{unit_id(origtext)}'>#{rendering}</mrow>\n"\
              "#{unitsml(units, origtext, normtext, quantity, name)}")
  end
  dedup_ids(xml)
end

#mathmlsymbol(units, normalise) ⇒ Object



45
46
47
48
49
50
51
52
53
54
# File 'lib/asciimath2unitsml/render.rb', line 45

def mathmlsymbol(units, normalise)
  exp = units.map do |u|
    if u[:multiplier] then u[:multiplier] == "*" ? @multiplier[:mathml] : "<mo>#{u[:multiplier]}</mo>"
    elsif u[:unit].nil? && u[:prefix]
      %(<mi mathvariant='normal'>#{htmlent(@prefixes[u[:prefix]].html)}</mi>)
    else
      mathmlsymbol1(u, normalise)
    end
  end.join("")
end

#mathmlsymbol1(u, normalise) ⇒ Object



56
57
58
59
60
61
62
63
64
65
# File 'lib/asciimath2unitsml/render.rb', line 56

def mathmlsymbol1(u, normalise)
  base = render(normalise ? @units[u[:unit]].symbolid : u[:unit], :mathml)
  if u[:prefix]
    prefix = htmlent(@prefixes[u[:prefix]].html)
    base = base.match(/<mi mathvariant='normal'>/) ?
      base.sub(/<mi mathvariant='normal'>/, "<mi mathvariant='normal'>#{prefix}") :
      "<mrow><mi mathvariant='normal'>#{prefix}#{base}</mrow>"
  end
  mathmlsymbol_exponent(u, base)
end

#mathmlsymbol_exponent(u, base) ⇒ Object



67
68
69
70
71
72
73
74
75
# File 'lib/asciimath2unitsml/render.rb', line 67

def mathmlsymbol_exponent(u, base)
  if u[:display_exponent] == "0.5"
    base = "<msqrt>#{base}</msqrt>"
  elsif u[:display_exponent]
    exp = "<mn>#{u[:display_exponent]}</mn>".sub(/<mn>-/, "<mo>&#x2212;</mo><mn>")
    base = "<msup><mrow>#{base}</mrow><mrow>#{exp}</mrow></msup>"
  end
  base
end

#mathmlsymbolwrap(units, normalise) ⇒ Object



77
78
79
80
81
# File 'lib/asciimath2unitsml/render.rb', line 77

def mathmlsymbolwrap(units, normalise)
  <<~END
  <math xmlns='#{MATHML_NS}'><mrow>#{mathmlsymbol(units, normalise)}</mrow></math>
  END
end

#multiplier(x) ⇒ Object



3
4
5
6
7
8
9
10
11
12
# File 'lib/asciimath2unitsml/render.rb', line 3

def multiplier(x)
  case x
  when :space
    { html: "&#xA0;", mathml: "<mo rspace='thickmathspace'>&#x2062;</mo>" }
  when :nospace
    { html: "", mathml: "<mo>&#x2062;</mo>" }
  else
    { html: HTMLEntities.new.encode(x), mathml: "<mo>#{HTMLEntities.new.encode(x)}</mo>" }
  end
end

#normalise_units(units) ⇒ Object



27
28
29
30
31
32
33
34
# File 'lib/asciimath2unitsml/unit.rb', line 27

def normalise_units(units)
  units.map do |u|
    u1 = u.dup
    u1[:multiplier] and u1[:multiplier] = "*"
    u1[:exponent] and u1[:display_exponent] = u1[:exponent]
    u1
  end
end

#parse(x) ⇒ Object



111
112
113
114
115
116
117
118
119
# File 'lib/asciimath2unitsml/parse.rb', line 111

def parse(x)
  text = Array(x.split(/,\s*/))
  units = @parser.parse!(text[0])
  if !units || Rsec::INVALID[units]
    raise Rsec::SyntaxError.new "error parsing UnitsML expression", x, 1, 0
  end
  Rsec::Fail.reset
  postprocess(units, text)
end

#parserObject



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/asciimath2unitsml/parse.rb', line 86

def parser
  prefix2 = /#{@prefixes.keys.select { |x| x.size == 2 }.join("|")}/.r
  prefix1 = /#{@prefixes.keys.select { |x| x.size == 1 }.join("|")}/.r
  unit_keys = @units.keys.reject do |k|
    /\*|\^|\/|^1$/.match(k) || @units[k].prefixed
  end.map { |k| Regexp.escape(k) }
  unit1 = /#{unit_keys.sort_by(&:length).reverse.join("|")}/.r
  exponent = /\^\(-?\d+\)/.r.map { |m| m.sub(/\^/, "").gsub(/[()]/, "") } |
    /\^-?\d+/.r.map { |m| m.sub(/\^/, "") }
  multiplier = %r{\*|//|/}.r.map { |x| { multiplier: x[0] } }
  unit = 
    seq("sqrt(", unit1, ")") { |x| { prefix: nil, unit: x[1], display_exponent: "0.5" } } |
    seq("sqrt(", prefix1, unit1, ")") { |x| { prefix: x[1], unit: x[2], display_exponent: "0.5" } } |
    seq("sqrt(", prefix2, unit1, ")") { |x| { prefix: x[1], unit: x[2], display_exponent: "0.5" } } |
    seq(unit1, exponent._? & multiplier) { |x| { prefix: nil, unit: x[0], display_exponent: (x[1][0] )} } |
    seq(unit1, exponent._?).eof { |x| { prefix: nil, unit: x[0], display_exponent: (x[1][0] )} } |
    seq(prefix1, unit1, exponent._? ) { |x| { prefix: x[0], unit: x[1], display_exponent: (x[2][0] ) } } |
    seq(prefix2, unit1, exponent._? ) { |x| { prefix: x[0], unit: x[1], display_exponent: (x[2][0] ) } } |
    "1".r.map { |_| { prefix: nil, unit: "1", display_exponent: nil } } 
  units = seq(prefix2, "-") { |x| [{ prefix: x[0], unit: nil, display_exponent: nil }] } |
    seq(prefix1, "-") { |x| [{ prefix: x[0], unit: nil, display_exponent: nil }] } |
    unit.join(multiplier)
  parser = units.eof
end

#postprocess(units, text) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
# File 'lib/asciimath2unitsml/parse.rb', line 121

def postprocess(units, text)
  units = postprocess1(units)
  quantity = text[1..-1]&.select { |x| /^quantity:/.match(x) }&.first&.sub(/^quantity:\s*/, "")
  name = text[1..-1]&.select { |x| /^name:/.match(x) }&.first&.sub(/^name:\s*/, "")
  symbol = text[1..-1]&.select { |x| /^symbol:/.match(x) }&.first&.sub(/^symbol:\s*/, "")
  normtext = units_only(units).each.map do |u|
    exp = u[:exponent] && u[:exponent] != "1" ? "^#{u[:exponent]}" : ""
    "#{u[:prefix]}#{u[:unit]}#{exp}"
  end.join("*")
  [units, text[0], normtext, quantity, name, symbol]
end

#postprocess1(units) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/asciimath2unitsml/parse.rb', line 133

def postprocess1(units)
  inverse = false
  units.each_with_object([]) do |u, m| 
    if u[:multiplier]
      inverse = (u[:multiplier] == "/")
    else
      u[:exponent] = inverse ? "-#{u[:display_exponent] || '1'}" : u[:display_exponent]
      u[:exponent] = u[:exponent]&.sub(/^--+/, "")
    end
    m << u
  end
end

#prefix(units) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/asciimath2unitsml/conv.rb', line 42

def prefix(units)
  units.map { |u| u[:prefix] }.reject { |u| u.nil? }.uniq.map do |p|
    <<~END
    <Prefix xmlns='#{UNITSML_NS}' prefixBase='#{@prefixes[p].base}'
            prefixPower='#{@prefixes[p].power}' xml:id='#{@prefixes[p].id}'>
      <PrefixName xml:lang="en">#{@prefixes[p].name}</PrefixName>
      <PrefixSymbol type="ASCII">#{@prefixes[p].ascii}</PrefixSymbol>
      <PrefixSymbol type="unicode">#{@prefixes[p].unicode}</PrefixSymbol>
      <PrefixSymbol type="LaTeX">#{@prefixes[p].latex}</PrefixSymbol>
      <PrefixSymbol type="HTML">#{htmlent @prefixes[p].html}</PrefixSymbol>
    </Prefix>
    END
  end.join("\n")
end

#quantity(normtext, quantity) ⇒ Object



158
159
160
161
162
163
164
165
166
167
# File 'lib/asciimath2unitsml/conv.rb', line 158

def quantity(normtext, quantity)
  return unless @units[normtext] && @units[normtext].quantities.size == 1 || @quantities[quantity]
  id = quantity || @units[normtext].quantities.first
  dim = %( dimensionURL="##{@units[normtext].dimension}") if @units[normtext]&.dimension
  <<~END
  <Quantity xmlns='#{UNITSML_NS}' xml:id="#{id}"#{dim} quantityType="base">
  #{quantityname(id)}
  </Quantity>
  END
end

#quantityname(id) ⇒ Object



150
151
152
153
154
155
156
# File 'lib/asciimath2unitsml/conv.rb', line 150

def quantityname(id)
  ret = ""
  @quantities[id].names.each do |q|
    ret += %(<QuantityName xml:lang="en-US">#{q}</QuantityName>)
  end
  ret
end

#read_yaml(path) ⇒ Object



5
6
7
# File 'lib/asciimath2unitsml/parse.rb', line 5

def read_yaml(path)
  validate_yaml(symbolize_keys(YAML.load_file(File.join(File.join(File.dirname(__FILE__), path)))), path)
end

#render(unit, style) ⇒ Object



14
15
16
# File 'lib/asciimath2unitsml/render.rb', line 14

def render(unit, style)
  @symbols[unit][style] || unit
end

#render_ambig_units(u) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/asciimath2unitsml/parse.rb', line 209

def render_ambig_units(u)
  maxcols = 0
  u.each { |_, v| maxcols = v.size if maxcols < v.size }
  puts %([cols="#{maxcols + 1}*"]\n|===\n|Symbol | Unit + ID #{"| " * (maxcols - 1)}\n)
  puts "\n\n"
  u.keys.sort_by { |a| [-u[a].size, a.gsub(%r{\&[^;]+;}, "").gsub(/[^A-Za-z]/, "").downcase] }.each do |k|
    print "| #{html2adoc(k)} "
    u[k].sort_by { |v1| v1.size }.each { |v1| print "| #{@units[v1].name}: `#{v1}` " }
    puts "#{"| " * (maxcols - u[k].size) }\n"
  end
  puts "|===\n"
end

#rootunits(units) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/asciimath2unitsml/unit.rb', line 68

def rootunits(units)
  return if units_only(units).any? { |x| x[:unit].nil? }
  return if units.size == 1 && !units[0][:prefix]
  exp = units_only(units).map do |u|
    prefix = " prefix='#{u[:prefix]}'" if u[:prefix]
    exponent = " powerNumerator='#{u[:exponent]}'" if u[:exponent] && u[:exponent] != "1"
    "<EnumeratedRootUnit unit='#{@units[u[:unit]].name}'#{prefix}#{exponent}/>"
  end.join("\n")
  <<~END
  <RootUnits>#{exp}</RootUnits>
  END
end

#symbol_key(v) ⇒ Object



62
63
64
65
66
67
# File 'lib/asciimath2unitsml/parse.rb', line 62

def symbol_key(v)
  symbol = v[:unit_symbols]&.each_with_object([]) { |s, m| m << (s["id"] || s[:id]) } || 
    v.dig(:symbol, :ascii) || v[:symbol] #|| v[:short]
  symbol = [symbol] if !symbol.nil? && v[:unit_symbols] && !symbol.is_a?(Array)
  symbol
end

#symbolize_keys(hash) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/asciimath2unitsml/parse.rb', line 69

def symbolize_keys(hash)
  return hash if hash.is_a? String
  hash.inject({}) do |result, (key, value)|
    new_key = case key
              when String then key.to_sym
              else key
              end
    new_value = case value
                when Hash then symbolize_keys(value)
                when Array then value.map { |m| symbolize_keys(m) }
                else value
                end
    result[new_key] = new_value
    result
  end
end

#unit(units, origtext, normtext, dims, name) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/asciimath2unitsml/unit.rb', line 13

def unit(units, origtext, normtext, dims, name)
  return if units_only(units).any? { |x| x[:unit].nil? }
  dimid = dim_id(dims)
  norm_units = normalise_units(units)
  <<~END
  <Unit xmlns='#{UNITSML_NS}' xml:id='#{unit_id(normtext)}'#{dimid ? " dimensionURL='##{dimid}'" : ""}>
  #{unitsystem(units)}
  #{unitname(norm_units, normtext, name)}
  #{unitsymbol(norm_units)}
  #{rootunits(units)}
  </Unit>
  END
end

#unit_id(text) ⇒ Object



7
8
9
10
11
# File 'lib/asciimath2unitsml/unit.rb', line 7

def unit_id(text)
  text = text.gsub(/[()]/, "")
  /-$/.match(text) and return @prefixes[text.sub(/-$/, "")].id
  "U_" + (@units[text] ? @units[text].id.gsub(/'/, "_") : text.gsub(/\*/, ".").gsub(/\^/, ""))
end

#unitname(units, text, name) ⇒ Object



51
52
53
54
# File 'lib/asciimath2unitsml/unit.rb', line 51

def unitname(units, text, name)
  name ||= @units[text] ? @units[text].name : compose_name(units, text)
  "<UnitName xml:lang='en'>#{name}</UnitName>"
end

#units2dimensions(units) ⇒ Object



79
80
81
82
83
84
85
86
87
88
# File 'lib/asciimath2unitsml/conv.rb', line 79

def units2dimensions(units)
  norm = decompose_units(units)
  return if norm.any? { |u| u[:unit] == "unknown" || u[:prefix] == "unknown" || u[:unit].nil? }
  norm.map do |u|
    { dimension: U2D[u[:unit]][:dimension],
      unit: u[:unit],
      exponent: u[:exponent] || 1,
      symbol: U2D[u[:unit]][:symbol] } 
  end.sort { |a, b| U2D[a[:unit]][:order] <=> U2D[b[:unit]][:order] }
end

#units_only(units) ⇒ Object



3
4
5
# File 'lib/asciimath2unitsml/unit.rb', line 3

def units_only(units)
  units.reject { |u| u[:multiplier] }
end

#unitsml(units, origtext, normtext, quantity, name) ⇒ Object



187
188
189
190
191
192
193
194
195
196
# File 'lib/asciimath2unitsml/conv.rb', line 187

def unitsml(units, origtext, normtext, quantity, name)
  dims = units2dimensions(units)
  <<~END
  #{unit(units, origtext, normtext, dims, name)}
  #{prefix(units)}
  #{dimension(normtext)}
  #{dimension_components(dims)}
  #{quantity(normtext, quantity)}
  END
end

#unitsymbol(units) ⇒ Object



61
62
63
64
65
66
# File 'lib/asciimath2unitsml/unit.rb', line 61

def unitsymbol(units)
  <<~END
  <UnitSymbol type="HTML">#{htmlsymbol(units, true)}</UnitSymbol>
  <UnitSymbol type="MathML">#{mathmlsymbolwrap(units, true)}</UnitSymbol>
  END
end

#unitsystem(units) ⇒ Object

kg exception



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/asciimath2unitsml/unit.rb', line 37

def unitsystem(units)
  return if units_only(units).any? { |x| x[:unit].nil? }
  ret = []
  units = units_only(units)
  units.any? { |x| @units[x[:unit]].system_name != "SI" } and
    ret << "<UnitSystem name='not_SI' type='not_SI' xml:lang='en-US'/>"
  if units.any? { |x| @units[x[:unit]].system_name == "SI" }
    base = units.size == 1 && @units[units[0][:unit]].system_type == "SI-base"
    base = true if units.size == 1 && units[0][:unit] == "g" && units[0][:prefix] == "k"
    ret << "<UnitSystem name='SI' type='#{base ? "SI_base" : "SI_derived"}' xml:lang='en-US'/>"
  end
  ret.join("\n")
end

#validate_symbols(m, v) ⇒ Object



44
45
46
47
48
49
50
51
52
53
# File 'lib/asciimath2unitsml/parse.rb', line 44

def validate_symbols(m, v)
  symbol = symbol_key(v)
  !symbol.nil? or raise StandardError.new "No symbol provided for unit: #{v}"
  Array(symbol)&.each do |s|
    m[s] && s != "1" and
      raise StandardError.new "symbol #{s} is not unique in #{v}: already used for #{m[s]}"
    m[s] = v
  end
  m 
end

#validate_unit(v) ⇒ Object



34
35
36
37
38
39
40
41
42
# File 'lib/asciimath2unitsml/parse.rb', line 34

def validate_unit(v)
  if v[:quantity_reference]
    v[:quantity_reference].is_a?(Array) or
      raise StandardError.new "No quantity_reference array provided for unit: #{v}"
  end
  if v[:unit_name]
    v[:unit_name].is_a?(Array) or raise StandardError.new "No unit_name array provided for unit: #{v}"
  end
end

#validate_unit_symbol_cardinality(us, k) ⇒ Object



55
56
57
58
59
60
# File 'lib/asciimath2unitsml/parse.rb', line 55

def validate_unit_symbol_cardinality(us, k)
  return true if us.nil?
  !us[:id].nil? && !us[:ascii].nil? && !us[:html].nil? && !us[:mathml].nil? && !us[:latex].nil? &&
    !us[:unicode].nil? and return true
  raise StandardError.new "malformed unit_symbol for #{k}: #{us}"
end

#validate_yaml(hash, path) ⇒ Object



23
24
25
26
27
28
29
30
31
32
# File 'lib/asciimath2unitsml/parse.rb', line 23

def validate_yaml(hash, path)
  return hash if path == "../unitsdb/quantities.yaml"
  return hash if path == "../unitsdb/dimensions.yaml"
  hash.each_with_object({}) do |(k, v), m|
    path == "../unitsdb/units.yaml" and validate_unit(v)
    m = validate_symbols(m, v)
    v[:unit_symbols]&.each { |s| validate_unit_symbol_cardinality(s, k) }
  end
  hash
end