Class: Asciimath2UnitsML::Conv

Inherits:
Object
  • Object
show all
Includes:
Rsec::Helpers
Defined in:
lib/asciimath2unitsml/conv.rb,
lib/asciimath2unitsml/read.rb,
lib/asciimath2unitsml/unit.rb,
lib/asciimath2unitsml/parse.rb,
lib/asciimath2unitsml/render.rb,
lib/asciimath2unitsml/validate.rb,
lib/asciimath2unitsml/dimensions.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
Dim2D =
{
  "dim_L" => U2D["m"],
  "dim_M" => U2D["g"],
  "dim_T" => U2D["s"],
  "dim_I" => U2D["A"],
  "dim_Theta" => U2D["K"],
  "dim_N" => U2D["mol"],
  "dim_J" => U2D["cd"],
  "dim_phi" => U2D["deg"],
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Conv

Returns a new instance of Conv.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/asciimath2unitsml/conv.rb', line 19

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
  @dimensions = flip_name_and_symbols(@dimensions_id)
  @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.merge(@dimensions).each_with_object({}) do |(_k, v), m|
    v.symbolids.each { |x| m[x] = v.symbols_hash[x] }
  end
  @parser, @dim_parser = parsers
  @multiplier = multiplier(options[:multiplier] || "\u22c5")
end

Instance Method Details

#ambig_unitsObject



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

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.each_key { |k| u[k] = u[k].unshift(k) if @symbols.dig(k, :html) == k }
  render_ambig_units(u)
end

#asciimath2mathml(expression) ⇒ Object



174
175
176
177
178
# File 'lib/asciimath2unitsml/parse.rb', line 174

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



115
116
117
118
# File 'lib/asciimath2unitsml/parse.rb', line 115

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

#combine_prefixes(p1, p2) ⇒ Object



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

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



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

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



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/asciimath2unitsml/conv.rb', line 106

def decompose_unit(u)
  if u[:unit].nil? || u[:unit] == "g" ||
      @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|
      prefix = if !k[:prefix].nil? && !k[:prefix].empty?
                 combine_prefixes(@prefixes_id[k[:prefix]],
                                  @prefixes[u[:prefix]])
               else
                 u[:prefix]
               end
      m << { prefix: 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



65
66
67
# File 'lib/asciimath2unitsml/conv.rb', line 65

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

#dedup_ids(xml) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/asciimath2unitsml/parse.rb', line 160

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

        n.remove
      end
    end
  end
  xml
end

#delimspace(rendering, elem) ⇒ Object

if previous sibling’s last descendent non-whitespace is MathML and mn or mi, no space



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/asciimath2unitsml/parse.rb', line 144

def delimspace(rendering, elem)
  prec_text_elem =
    elem.xpath("./preceding-sibling::*[namespace-uri() = '#{MATHML_NS}']/"\
               "descendant::text()[normalize-space()!='']"\
               "[last()]/parent::*").last
  return "" if prec_text_elem.nil? ||
    !%w(mn mi).include?(prec_text_elem&.name)

  text = HTMLEntities.new.encode(Nokogiri::XML("<mrow>#{rendering}</mrow>")
    .text.strip)
  if /\p{L}|\p{N}/.match?(text)
    "<mo rspace='thickmathspace'>&#x2062;</mo>"
  else "<mo>&#x2062;</mo>"
  end
end

#dim_id(dims) ⇒ Object



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

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.dig(d[:unit], :symbol) || Dim2D.dig(d[:id], :symbol)) +
      (d[:exponent] == 1 ? "" : float_to_display(d[:exponent]))
  end.join("")
end

#dimension(normtext) ⇒ Object



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

def dimension(normtext)
  return unless @units[normtext]&.dimension

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

#dimension1(dim) ⇒ Object



63
64
65
66
# File 'lib/asciimath2unitsml/dimensions.rb', line 63

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

#dimension_components(dims) ⇒ Object



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

def dimension_components(dims)
  return if dims.nil? || dims.empty?

  <<~XML
    <Dimension xmlns='#{UNITSML_NS}' xml:id="#{dim_id(dims)}">
    #{dims.map { |u| dimension1(u) }.join("\n")}
    </Dimension>
  XML
end

#dimensions_parser(exponent, multiplier) ⇒ Object



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

def dimensions_parser(exponent, multiplier)
  dim1 = /#{@dimensions.keys.sort_by(&:length).reverse.join("|")}/.r
  dimension =
    seq("sqrt(", dim1, ")") { |x| { dim: x[1], display_exponent: "0.5" } } |
    seq(dim1, exponent._? & (multiplier | ")".r)) { |x| { dim: x[0], display_exponent: (x[1][0]) } } |
    seq(dim1, exponent._?).eof { |x| { dim: x[0], display_exponent: (x[1][0]) } }
  dimensions1 = "(".r >> lazy { dimensions } << ")" | dimension
  dimensions = dimensions1.join(multiplier) # rubocop:disable Style/RedundantAssignment
  dimensions
end

#dimid2dimensions(normtext) ⇒ Object



87
88
89
90
91
92
93
# File 'lib/asciimath2unitsml/dimensions.rb', line 87

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

#display_exp(unit) ⇒ Object



97
98
99
# File 'lib/asciimath2unitsml/parse.rb', line 97

def display_exp(unit)
  unit[:exponent] && unit[:exponent] != "1" ? "^#{unit[:exponent]}" : ""
end

#embeddedmathml(mathml) ⇒ Object



180
181
182
183
184
185
# File 'lib/asciimath2unitsml/parse.rb', line 180

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



26
27
28
29
30
31
32
# File 'lib/asciimath2unitsml/read.rb', line 26

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



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

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(float) ⇒ Object



46
47
48
# File 'lib/asciimath2unitsml/conv.rb', line 46

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

#gather_dimensions(units) ⇒ Object



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

def gather_dimensions(units)
  units.sort_by { |a| a[:dim] }.each_with_object([]) do |k, m| 
    if m.empty? || m[-1][:dim] != k[:dim] then m << k
    else
      m[-1] = {
        dim: m[-1][:dim],
        exponent: (k[:exponent]&.to_f || 1) +
          (m[-1][:exponent]&.to_f || 1),
      }
    end
  end
end

#gather_units(units) ⇒ Object



69
70
71
72
73
# File 'lib/asciimath2unitsml/conv.rb', line 69

def gather_units(units)
  if units[0][:dim] then gather_dimensions(units)
  else gather_units1(units)
  end
end

#gather_units1(units) ⇒ Object



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

def gather_units1(units)
  units.sort_by { |a| a[: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(elem) ⇒ Object



217
218
219
220
221
# File 'lib/asciimath2unitsml/parse.rb', line 217

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

#htmlent(xml) ⇒ Object



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

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

#htmlsymbol(units, normalise) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/asciimath2unitsml/render.rb', line 25

def htmlsymbol(units, normalise)
  units.map do |u|
    if u[:multiplier]
      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(unit, base) ⇒ Object



39
40
41
42
43
44
45
46
47
# File 'lib/asciimath2unitsml/render.rb', line 39

def htmlsymbol_exponent(unit, base)
  if unit[:display_exponent] == "0.5"
    base = "&#x221a;#{base}"
  elsif unit[:display_exponent]
    exp = "<sup>#{unit[: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



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

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, multiplier =
      parse(text)
    rendering = if symbol
                  embeddedmathml(asciimath2mathml(symbol))
                else
                  mathmlsymbol(units, false, multiplier)
                end
    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, multiplier = nil) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/asciimath2unitsml/render.rb', line 49

def mathmlsymbol(units, normalise, multiplier = nil)
  multiplier = multiplier ? "<mo>#{multiplier}</mo>" : @multiplier[:mathml]
  units.map do |u|
    if u[:multiplier]
      u[:multiplier] == "*" ? multiplier : "<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(unit, normalise) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/asciimath2unitsml/render.rb', line 62

def mathmlsymbol1(unit, normalise)
  base = if unit[:dim]
           render(normalise ? @dimensions[unit[:dim]].symbolid : unit[:dim],
                  :mathml)
         else
           render(normalise ? @units[unit[:unit]].symbolid : unit[:unit],
                  :mathml)
         end
  unit[:prefix] and base = mathmlsymbol1_prefixed(unit, base)
  mathmlsymbol_exponent(unit, base)
end

#mathmlsymbol1_prefixed(unit, base) ⇒ Object



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

def mathmlsymbol1_prefixed(unit, base)
  prefix = htmlent(@prefixes[unit[:prefix]].html)
  if /<mi mathvariant='normal'>/.match?(base)
    base.sub(/<mi mathvariant='normal'>/,
             "<mi mathvariant='normal'>#{prefix}")
  else
    "<mrow><mi mathvariant='normal'>#{prefix}#{base}</mrow>"
  end
end

#mathmlsymbol_exponent(unit, base) ⇒ Object



84
85
86
87
88
89
90
91
92
93
# File 'lib/asciimath2unitsml/render.rb', line 84

def mathmlsymbol_exponent(unit, base)
  if unit[:display_exponent] == "0.5"
    base = "<msqrt>#{base}</msqrt>"
  elsif unit[:display_exponent]
    exp = "<mn>#{unit[: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



95
96
97
98
99
# File 'lib/asciimath2unitsml/render.rb', line 95

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

#multiplier(val) ⇒ Object



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

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

#normalise_units(units) ⇒ Object



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

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(expr) ⇒ Object



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

def parse(expr)
  text = Array(expr.split(/,\s*/))
  if /dim_/.match?(text[0]) then parse_dimensions(text)
  else parse_units(text)
  end
end

#parse_dimensions(text) ⇒ Object



65
66
67
68
69
70
71
72
73
# File 'lib/asciimath2unitsml/parse.rb', line 65

def parse_dimensions(text)
  units = @dim_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, false)
end

#parse_units(text) ⇒ Object



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

def parse_units(text)
  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, true)
end

#parsersObject



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

def parsers
  exponent = /\^\(-?\d+\)/.r.map { |m| m.sub(/\^/, "").gsub(/[()]/, "") } |
    /\^-?\d+/.r.map { |m| m.sub(/\^/, "") }
  multiplier = %r{\*|//|/}.r.map { |x| { multiplier: x[0] } }
  units = units_parse(exponent, multiplier)
  dimensions = dimensions_parser(exponent, multiplier)
  [units.eof, dimensions.eof]
end

#postprocess(units, text, is_units) ⇒ Object



75
76
77
78
79
80
81
# File 'lib/asciimath2unitsml/parse.rb', line 75

def postprocess(units, text, is_units)
  units = postprocess1(units.flatten)
  normtext = postprocess_normtext(units, is_units)
  [units, text[0], normtext, postprocess_extr(text, "quantity"),
   postprocess_extr(text, "name"), postprocess_extr(text, "symbol"),
   postprocess_extr(text, "multiplier")]
end

#postprocess1(units) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/asciimath2unitsml/parse.rb', line 101

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

#postprocess_extr(text, name) ⇒ Object



91
92
93
94
95
# File 'lib/asciimath2unitsml/parse.rb', line 91

def postprocess_extr(text, name)
  text[1..-1]&.select do |x|
    /^#{name}:/.match(x)
  end&.first&.sub(/^#{name}:\s*/, "")
end

#postprocess_normtext(units, is_units) ⇒ Object



83
84
85
86
87
88
89
# File 'lib/asciimath2unitsml/parse.rb', line 83

def postprocess_normtext(units, is_units)
  units_only(units).each.map do |u|
    if is_units then "#{u[:prefix]}#{u[:unit]}#{display_exp(u)}"
    else "#{u[:dim]}#{display_exp(u)}"
    end
  end.join("*")
end

#prefix(units) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/asciimath2unitsml/conv.rb', line 50

def prefix(units)
  units.map { |u| u[:prefix] }.reject(&:nil?).uniq.map do |p|
    <<~XML
      <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>
    XML
  end.join("\n")
end

#quantity(normtext, quantity) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/asciimath2unitsml/conv.rb', line 146

def quantity(normtext, quantity)
  return unless @units[normtext] && @units[normtext].quantities.size == 1 ||
    @quantities[quantity]

  id = quantity || @units[normtext].quantities.first
  @units[normtext]&.dimension and
    dim = %( dimensionURL="##{@units[normtext].dimension}")
  <<~XML
    <Quantity xmlns='#{UNITSML_NS}' xml:id="#{id}"#{dim} quantityType="base">
    #{quantityname(id)}
    </Quantity>
  XML
end

#quantityname(id) ⇒ Object



138
139
140
141
142
143
144
# File 'lib/asciimath2unitsml/conv.rb', line 138

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



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

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



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

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

#render_ambig_units(u) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/asciimath2unitsml/parse.rb', line 201

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 do |a|
    [-u[a].size, a.gsub(%r{&[^;]+;}, "")
      .gsub(/[^A-Za-z]/, "").downcase]
  end.each do |k|
    print "| #{html2adoc(k)} "
    u[k].sort_by(&:size).each { |v1| print "| #{@units[v1].name}: `#{v1}` " }
    puts "#{'| ' * (maxcols - u[k].size)}\n"
  end
  puts "|===\n"
end

#rootunits(units) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/asciimath2unitsml/unit.rb', line 72

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]
    u[:exponent] && u[:exponent] != "1" and
      arg = " powerNumerator='#{u[:exponent]}'"
    "<EnumeratedRootUnit unit='#{@units[u[:unit]].name}'#{prefix}#{arg}/>"
  end.join("\n")
  <<~XML
    <RootUnits>#{exp}</RootUnits>
  XML
end

#symbol_key(val) ⇒ Object



50
51
52
53
54
55
56
57
# File 'lib/asciimath2unitsml/validate.rb', line 50

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

#symbolize_keys(hash) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/asciimath2unitsml/read.rb', line 8

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
26
# 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)
  <<~XML
    <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>
  XML
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



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

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



39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/asciimath2unitsml/dimensions.rb', line 39

def units2dimensions(units)
  norm = decompose_units(units)
  return units2dimensions_dim_input(norm) if norm[0][:dim]
  return if norm.any? do |u|
    u[:unit] == "unknown" || u[:prefix] == "unknown" || u[:unit].nil?
  end

  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

#units2dimensions_dim_input(norm) ⇒ Object



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

def units2dimensions_dim_input(norm)
  norm.map do |u|
    { dimension: Dim2D[u[:dim]][:dimension],
      exponent: u[:exponent] || 1,
      id: u[:dim],
      symbol: Dim2D[u[:dim]][:symbol] }
  end.sort { |a, b| Dim2D[a[:id]][:order] <=> Dim2D[b[:id]][: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

#units_parse(exponent, multiplier) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/asciimath2unitsml/parse.rb', line 24

def units_parse(exponent, multiplier)
  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

  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 | ")".r)) { |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 } }
  units1 = "(".r >> lazy { units } << ")" | unit
  units = seq(prefix2, "-") { |x| [{ prefix: x[0], unit: nil, display_exponent: nil }] } | # rubocop:disable Style/RedundantAssignment
    seq(prefix1, "-") { |x| [{ prefix: x[0], unit: nil, display_exponent: nil }] } |
    units1.join(multiplier)
  units
end

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



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

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

#unitsymbol(units) ⇒ Object



65
66
67
68
69
70
# File 'lib/asciimath2unitsml/unit.rb', line 65

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

#unitsystem(units) ⇒ Object

kg exception



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

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(acc, val) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/asciimath2unitsml/validate.rb', line 28

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

#validate_unit(unit) ⇒ Object



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

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

#validate_unit_symbol_cardinality(sym, key) ⇒ Object



41
42
43
44
45
46
47
48
# File 'lib/asciimath2unitsml/validate.rb', line 41

def validate_unit_symbol_cardinality(sym, key)
  return true if sym.nil?

  !sym[:id].nil? && !sym[:ascii].nil? && !sym[:html].nil? &&
    !sym[:mathml].nil? && !sym[:latex].nil? &&
    !sym[:unicode].nil? and return true
  raise StandardError.new "malformed unit_symbol for #{key}: #{sym}"
end

#validate_yaml(hash, path) ⇒ Object



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

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