Class: Asciimath2UnitsML::Conv

Inherits:
Object
  • Object
show all
Includes:
Rsec::Helpers
Defined in:
lib/asciimath2unitsml/conv.rb,
lib/asciimath2unitsml/parse.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" },
  "mol" => { dimension: "AmountOfSubstance", order: 6, symbol: "N" },
  "cd" => { dimension: "LuminousIntensity", order: 7, symbol: "J" },
}

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Conv

Returns a new instance of Conv.



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

def initialize(options = {})
  @prefixes_id = read_yaml("../unitsdb/prefixes.yaml")
  @prefixes = flip_name_and_id(@prefixes_id)
  @quantities = read_yaml("../unitsdb/quantities.yaml")
  @units_id = read_yaml("../unitsdb/units.yaml")
  @units = flip_name_and_id(@units_id)
  @parser = parser
  @multiplier = multiplier(options[:multiplier] || "\u00b7")
end

Instance Method Details

#asciimath2mathml(expression) ⇒ Object



86
87
88
89
90
# File 'lib/asciimath2unitsml/parse.rb', line 86

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



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

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

#combine_prefixes(p1, p2) ⇒ Object



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

def combine_prefixes(p1, p2)
  return nil if p1.nil? && p2.nil?
  return p1[:symbol] if p2.nil?
  return p2[:symbol] if p1.nil?
  return "unknown" if p1[:base] != p2[:base]
  @prefixes.each do |p|
    return p[:symbol] 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



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

def compose_name(units, text)
  text
end

#dim_id(dims) ⇒ Object



156
157
158
159
# File 'lib/asciimath2unitsml/conv.rb', line 156

def dim_id(dims)
  return nil if dims.nil? || dims.empty?
  "D_" + dims.map { |d| U2D[d[:unit]][:symbol] + (d[:exponent] == 1 ? "" : d[:exponent].to_s) }.join("")
end

#dimension(dims) ⇒ Object



132
133
134
135
136
137
138
139
# File 'lib/asciimath2unitsml/conv.rb', line 132

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

#dimension1(u) ⇒ Object



152
153
154
# File 'lib/asciimath2unitsml/conv.rb', line 152

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

#flip_name_and_id(yaml) ⇒ Object



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

def flip_name_and_id(yaml)
  yaml.each_with_object({}) do |(k, v), m|
    next if v[:name].nil? || v[:name].empty?
    symbol = v[:symbol] || v[:short]
    m[symbol.to_sym] = v
    m[symbol.to_sym][:symbol] = symbol
    m[symbol.to_sym][:id] = k.to_s
  end
end

#gather_units(units) ⇒ Object



165
166
167
168
169
170
171
172
173
174
# File 'lib/asciimath2unitsml/conv.rb', line 165

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_i || 1) + (m[-1][:exponent]&.to_i || 1) }
    end
  end
end

#htmlsymbol(units) ⇒ Object



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

def htmlsymbol(units)
  units.map do |u|
    u[:exponent] and exp = "<sup>#{u[:exponent].sub(/-/, "&#x2212;")}</sup>"
    "#{u[:prefix]}#{u[:unit]}#{exp}"
  end.join(@multiplier[:html])
end

#MathML2UnitsML(xml) ⇒ Object

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



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

def MathML2UnitsML(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 = parse(text)
    delim = x&.previous_element&.name == "mn" ? "<mo rspace='thickmathspace'>&#x2062;</mo>" : ""
    x.replace("#{delim}<mrow xref='#{unit_id(text)}'>#{mathmlsymbol(units)}</mrow>\n#{unitsml(units, text)}")
  end
  xml
end

#mathmlsymbol(units) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
# File 'lib/asciimath2unitsml/conv.rb', line 87

def mathmlsymbol(units)
  exp = units.map do |u|
    base = "<mi mathvariant='normal'>#{u[:prefix]}#{u[:unit]}</mi>"
    if u[:exponent]
      exp = "<mn>#{u[:exponent]}</mn>".sub(/<mn>-/, "<mo>&#x2212;</mo><mn>")
      "<msup><mrow>#{base}</mrow><mrow>#{exp}</mrow></msup>"
    else
      base
    end
  end.join(@multiplier[:mathml])
end

#mathmlsymbolwrap(units) ⇒ Object



99
100
101
102
103
104
105
# File 'lib/asciimath2unitsml/conv.rb', line 99

def mathmlsymbolwrap(units)
  "  <math xmlns='\#{MATHML_NS}'>\n  <mrow>\#{mathmlsymbol(units)}</mrow>\n  </math>\n  END\nend\n"

#multiplier(x) ⇒ Object



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

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



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

def normalise_unit(u)
  if @units[u[:unit].to_sym][:type]&.include?("si-base") then u
  elsif !@units[u[:unit].to_sym][:bases] then { prefix: u[:prefix], unit: "unknown", exponent: u[:exponent] }
  else
    @units[u[:unit].to_sym][:bases].each_with_object([]) do |k, m|
      m << { prefix: k["prefix"] ?
             combine_prefixes(@prefixes_id[k["prefix"]], @prefixes[u[:prefix]]) : u[:prefix],
             unit: @units_id[k["id"].to_sym][:symbol],
             exponent: (k["power"]&.to_i || 1) * (u[:exponent]&.to_i || 1) }
    end
  end
end

#normalise_units(units) ⇒ Object



161
162
163
# File 'lib/asciimath2unitsml/conv.rb', line 161

def normalise_units(units)
  gather_units(units.map { |u| normalise_unit(u) }.flatten)
end

#parse(x) ⇒ Object



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

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

#parserObject



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

def parser
  prefix = /#{@prefixes.keys.join("|")}/.r
  unit_keys = @units.keys.reject do |k|
    @units[k][:type]&.include?("buildable") || /\*|\^/.match(k)
  end.map { |k| Regexp.escape(k) }
  unit1 = /#{unit_keys.sort_by(&:length).reverse.join("|")}/.r
  exponent = /\^-?\d+/.r.map { |m| m.sub(/\^/, "") }
  multiplier = /\*/.r
  unit = seq(unit1, exponent._?) { |x| { prefix: nil, unit: x[0], exponent: x[1][0] } } |
    seq(prefix, unit1, exponent._?) { |x| { prefix: x[0][0], unit: x[1], exponent: x[2][0] } }
  units_tail = seq(multiplier, unit) { |u| u[1] }
  units = seq(unit, units_tail.star) { |x| [x[0], x[1]].flatten }
  parser = units.eof
end

#prefix(units) ⇒ Object



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

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

#read_yaml(path) ⇒ Object



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

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

#rootunits(units) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/asciimath2unitsml/conv.rb', line 107

def rootunits(units)
  return if units.size == 1
  exp = units.map do |u|
    prefix = " prefix='#{u[:prefix]}'" if u[:prefix]
    exponent = " powerNumerator='#{u[:exponent]}'" if u[:exponent]
    "<EnumeratedRootUnit unit='#{@units[u[:unit].to_sym][:name]}'#{prefix}#{exponent}/>"
  end.join("\n")
  "  <RootUnits>\#{exp}</RootUnits>\n  END\nend\n"

#symbolize_keys(hash) ⇒ Object



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

def symbolize_keys(hash)
  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)
                else value
                end
    result[new_key] = new_value
    result
  end
end

#unit(units, text, dims) ⇒ Object



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

def unit(units, text, dims)
  dimid = dim_id(dims)
  "  <Unit xmlns='\#{UNITSML_NS}' xml:id='\#{unit_id(text)}'\#{dimid ? \" dimensionURL='#\#{dimid}'\" : \"\"}>\n  \#{unitsystem(units)}\n  \#{unitname(units, text)}\n  \#{unitsymbol(units)}\n  \#{rootunits(units)}\n  </Unit>\n  END\nend\n"

#unit_id(text) ⇒ Object



35
36
37
38
# File 'lib/asciimath2unitsml/conv.rb', line 35

def unit_id(text)
  "U_" +
    (@units[text.to_sym] ? @units[text.to_sym][:id] : text.gsub(/\*/, ".").gsub(/\^/, ""))
end

#unitname(units, text) ⇒ Object



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

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

#units2dimensions(units) ⇒ Object



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

def units2dimensions(units)
  norm = normalise_units(units)
  return if norm.any? { |u| u[:unit] == "unknown" || u[:prefix] == "unknown" }
  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

#unitsml(units, text) ⇒ Object



200
201
202
203
204
205
206
207
# File 'lib/asciimath2unitsml/conv.rb', line 200

def unitsml(units, text)
  dims = units2dimensions(units)
  "  \#{unit(units, text, dims)}\n  \#{prefix(units)}\n  \#{dimension(dims)}\n  END\nend\n"

#unitsymbol(units) ⇒ Object



73
74
75
76
77
78
# File 'lib/asciimath2unitsml/conv.rb', line 73

def unitsymbol(units)
  "  <UnitSymbol type=\"HTML\">\#{htmlsymbol(units)}</UnitSymbol>\n  <UnitSymbol type=\"MathML\">\#{mathmlsymbolwrap(units)}</UnitSymbol>\n  END\nend\n"

#unitsystem(units) ⇒ Object



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

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