unitsdb 2.1.1 → 2.2.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +8 -1
- data/.gitignore +2 -0
- data/.gitmodules +4 -3
- data/.rubocop.yml +13 -8
- data/.rubocop_todo.yml +217 -100
- data/CLAUDE.md +55 -0
- data/Gemfile +4 -1
- data/README.adoc +283 -16
- data/data/dimensions.yaml +1864 -0
- data/data/prefixes.yaml +874 -0
- data/data/quantities.yaml +3715 -0
- data/data/scales.yaml +97 -0
- data/data/schemas/dimensions-schema.yaml +153 -0
- data/data/schemas/prefixes-schema.yaml +155 -0
- data/data/schemas/quantities-schema.yaml +117 -0
- data/data/schemas/scales-schema.yaml +106 -0
- data/data/schemas/unit_systems-schema.yaml +116 -0
- data/data/schemas/units-schema.yaml +215 -0
- data/data/unit_systems.yaml +78 -0
- data/data/units.yaml +14052 -0
- data/exe/unitsdb +7 -1
- data/lib/unitsdb/cli.rb +42 -15
- data/lib/unitsdb/commands/_modify.rb +40 -4
- data/lib/unitsdb/commands/base.rb +6 -2
- data/lib/unitsdb/commands/check_si/si_formatter.rb +488 -0
- data/lib/unitsdb/commands/check_si/si_matcher.rb +487 -0
- data/lib/unitsdb/commands/check_si/si_ttl_parser.rb +103 -0
- data/lib/unitsdb/commands/check_si/si_updater.rb +254 -0
- data/lib/unitsdb/commands/check_si.rb +54 -35
- data/lib/unitsdb/commands/get.rb +11 -10
- data/lib/unitsdb/commands/normalize.rb +21 -7
- data/lib/unitsdb/commands/qudt/check.rb +150 -0
- data/lib/unitsdb/commands/qudt/formatter.rb +194 -0
- data/lib/unitsdb/commands/qudt/matcher.rb +746 -0
- data/lib/unitsdb/commands/qudt/ttl_parser.rb +403 -0
- data/lib/unitsdb/commands/qudt/update.rb +126 -0
- data/lib/unitsdb/commands/qudt/updater.rb +189 -0
- data/lib/unitsdb/commands/qudt.rb +82 -0
- data/lib/unitsdb/commands/release.rb +12 -9
- data/lib/unitsdb/commands/search.rb +12 -11
- data/lib/unitsdb/commands/ucum/check.rb +42 -29
- data/lib/unitsdb/commands/ucum/formatter.rb +2 -1
- data/lib/unitsdb/commands/ucum/matcher.rb +23 -9
- data/lib/unitsdb/commands/ucum/update.rb +14 -13
- data/lib/unitsdb/commands/ucum/updater.rb +40 -6
- data/lib/unitsdb/commands/ucum/xml_parser.rb +0 -2
- data/lib/unitsdb/commands/ucum.rb +44 -4
- data/lib/unitsdb/commands/validate/identifiers.rb +2 -4
- data/lib/unitsdb/commands/validate/qudt_references.rb +111 -0
- data/lib/unitsdb/commands/validate/references.rb +36 -19
- data/lib/unitsdb/commands/validate/si_references.rb +3 -5
- data/lib/unitsdb/commands/validate/ucum_references.rb +105 -0
- data/lib/unitsdb/commands/validate.rb +67 -11
- data/lib/unitsdb/commands.rb +20 -0
- data/lib/unitsdb/database.rb +90 -52
- data/lib/unitsdb/dimension.rb +1 -4
- data/lib/unitsdb/dimension_details.rb +0 -1
- data/lib/unitsdb/dimensions.rb +0 -2
- data/lib/unitsdb/errors.rb +7 -0
- data/lib/unitsdb/prefix.rb +0 -4
- data/lib/unitsdb/prefix_reference.rb +0 -2
- data/lib/unitsdb/prefixes.rb +0 -1
- data/lib/unitsdb/quantities.rb +0 -2
- data/lib/unitsdb/quantity.rb +0 -6
- data/lib/unitsdb/qudt.rb +100 -0
- data/lib/unitsdb/root_unit_reference.rb +0 -3
- data/lib/unitsdb/scale.rb +0 -4
- data/lib/unitsdb/scale_reference.rb +0 -2
- data/lib/unitsdb/scales.rb +0 -2
- data/lib/unitsdb/si_derived_base.rb +0 -2
- data/lib/unitsdb/ucum.rb +14 -10
- data/lib/unitsdb/unit.rb +0 -10
- data/lib/unitsdb/unit_reference.rb +0 -2
- data/lib/unitsdb/unit_system.rb +1 -3
- data/lib/unitsdb/unit_system_reference.rb +0 -2
- data/lib/unitsdb/unit_systems.rb +0 -2
- data/lib/unitsdb/units.rb +0 -2
- data/lib/unitsdb/utils.rb +32 -21
- data/lib/unitsdb/version.rb +5 -1
- data/lib/unitsdb.rb +62 -14
- data/unitsdb.gemspec +6 -3
- metadata +52 -13
- data/lib/unitsdb/commands/si_formatter.rb +0 -485
- data/lib/unitsdb/commands/si_matcher.rb +0 -470
- data/lib/unitsdb/commands/si_ttl_parser.rb +0 -100
- data/lib/unitsdb/commands/si_updater.rb +0 -212
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "set"
|
|
6
|
+
require "rdf"
|
|
7
|
+
require "rdf/turtle"
|
|
8
|
+
|
|
9
|
+
module Unitsdb
|
|
10
|
+
module Commands
|
|
11
|
+
module Qudt
|
|
12
|
+
class TtlParser
|
|
13
|
+
# QUDT 3.1.2 vocabulary URLs
|
|
14
|
+
QUDT_VOCABULARIES = {
|
|
15
|
+
units: "http://qudt.org/3.1.2/vocab/unit",
|
|
16
|
+
quantitykinds: "http://qudt.org/3.1.2/vocab/quantitykind",
|
|
17
|
+
dimensionvectors: "http://qudt.org/3.1.2/vocab/dimensionvector",
|
|
18
|
+
sou: "http://qudt.org/3.1.2/vocab/sou",
|
|
19
|
+
prefixes: "http://qudt.org/3.1.2/vocab/prefix",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# QUDT predicates
|
|
23
|
+
QUDT_PREDICATES = {
|
|
24
|
+
label: RDF::URI("http://www.w3.org/2000/01/rdf-schema#label"),
|
|
25
|
+
symbol: RDF::URI("http://qudt.org/schema/qudt/symbol"),
|
|
26
|
+
has_quantity_kind: RDF::URI("http://qudt.org/schema/qudt/hasQuantityKind"),
|
|
27
|
+
has_dimension_vector: RDF::URI("http://qudt.org/schema/qudt/hasDimensionVector"),
|
|
28
|
+
conversion_multiplier: RDF::URI("http://qudt.org/schema/qudt/conversionMultiplier"),
|
|
29
|
+
conversion_offset: RDF::URI("http://qudt.org/schema/qudt/conversionOffset"),
|
|
30
|
+
description: RDF::URI("http://purl.org/dc/elements/1.1/description"),
|
|
31
|
+
abbreviation: RDF::URI("http://qudt.org/schema/qudt/abbreviation"),
|
|
32
|
+
si_exact_match: RDF::URI("http://qudt.org/schema/qudt/siExactMatch"),
|
|
33
|
+
dimension_exponent_for_length: RDF::URI("http://qudt.org/schema/qudt/dimensionExponentForLength"),
|
|
34
|
+
dimension_exponent_for_mass: RDF::URI("http://qudt.org/schema/qudt/dimensionExponentForMass"),
|
|
35
|
+
dimension_exponent_for_time: RDF::URI("http://qudt.org/schema/qudt/dimensionExponentForTime"),
|
|
36
|
+
dimension_exponent_for_electric_current: RDF::URI("http://qudt.org/schema/qudt/dimensionExponentForElectricCurrent"),
|
|
37
|
+
dimension_exponent_for_thermodynamic_temperature: RDF::URI("http://qudt.org/schema/qudt/dimensionExponentForThermodynamicTemperature"),
|
|
38
|
+
dimension_exponent_for_amount_of_substance: RDF::URI("http://qudt.org/schema/qudt/dimensionExponentForAmountOfSubstance"),
|
|
39
|
+
dimension_exponent_for_luminous_intensity: RDF::URI("http://qudt.org/schema/qudt/dimensionExponentForLuminousIntensity"),
|
|
40
|
+
prefix_multiplier: RDF::URI("http://qudt.org/schema/qudt/prefixMultiplier"),
|
|
41
|
+
prefix_multiplier_sn: RDF::URI("http://qudt.org/schema/qudt/prefixMultiplierSN"),
|
|
42
|
+
ucum_code: RDF::URI("http://qudt.org/schema/qudt/ucumCode"),
|
|
43
|
+
rdf_type: RDF::URI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
|
|
44
|
+
dc_description: RDF::URI("http://purl.org/dc/terms/description"),
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# Parse QUDT vocabularies from TTL files or URLs
|
|
49
|
+
def parse_qudt_vocabularies(source_type: :url, ttl_dir: nil)
|
|
50
|
+
vocabularies = QudtVocabularies.new
|
|
51
|
+
|
|
52
|
+
QUDT_VOCABULARIES.each do |vocab_type, url|
|
|
53
|
+
puts "Parsing #{vocab_type} vocabulary..."
|
|
54
|
+
|
|
55
|
+
ttl_content = if source_type == :file && ttl_dir
|
|
56
|
+
read_ttl_file(ttl_dir, vocab_type)
|
|
57
|
+
else
|
|
58
|
+
download_ttl_content(url)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
graph = parse_ttl_content(ttl_content)
|
|
62
|
+
entities = extract_entities(graph, vocab_type, url)
|
|
63
|
+
|
|
64
|
+
case vocab_type
|
|
65
|
+
when :units
|
|
66
|
+
vocabularies.units = entities
|
|
67
|
+
when :quantitykinds
|
|
68
|
+
vocabularies.quantity_kinds = entities
|
|
69
|
+
when :dimensionvectors
|
|
70
|
+
vocabularies.dimension_vectors = entities
|
|
71
|
+
when :sou
|
|
72
|
+
vocabularies.systems_of_units = entities
|
|
73
|
+
when :prefixes
|
|
74
|
+
vocabularies.prefixes = entities
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts "Found #{entities.size} #{vocab_type}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
vocabularies
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get entities from vocabularies by type
|
|
84
|
+
def get_entities_from_qudt(entity_type, vocabularies)
|
|
85
|
+
case entity_type
|
|
86
|
+
when "units"
|
|
87
|
+
vocabularies.units
|
|
88
|
+
when "quantities"
|
|
89
|
+
vocabularies.quantity_kinds
|
|
90
|
+
when "dimensions"
|
|
91
|
+
vocabularies.dimension_vectors
|
|
92
|
+
when "unit_systems"
|
|
93
|
+
vocabularies.systems_of_units
|
|
94
|
+
when "prefixes"
|
|
95
|
+
vocabularies.prefixes
|
|
96
|
+
else
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Read TTL file from local directory
|
|
104
|
+
def read_ttl_file(ttl_dir, vocab_type)
|
|
105
|
+
# Map vocabulary types to actual filenames
|
|
106
|
+
filename_map = {
|
|
107
|
+
units: "unit.ttl",
|
|
108
|
+
quantitykinds: "quantitykind.ttl",
|
|
109
|
+
dimensionvectors: "dimensionvector.ttl",
|
|
110
|
+
sou: "sou.ttl",
|
|
111
|
+
prefixes: "prefix.ttl",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
filename = filename_map[vocab_type] || "#{vocab_type}.ttl"
|
|
115
|
+
file_path = File.join(ttl_dir, filename)
|
|
116
|
+
|
|
117
|
+
unless File.exist?(file_path)
|
|
118
|
+
raise "TTL file not found: #{file_path}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
File.read(file_path)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Download TTL content from URL
|
|
125
|
+
def download_ttl_content(url)
|
|
126
|
+
uri = URI(url)
|
|
127
|
+
max_redirects = 5
|
|
128
|
+
redirects = 0
|
|
129
|
+
|
|
130
|
+
loop do
|
|
131
|
+
Net::HTTP.start(uri.host, uri.port,
|
|
132
|
+
use_ssl: uri.scheme == "https") do |http|
|
|
133
|
+
request = Net::HTTP::Get.new(uri)
|
|
134
|
+
request["Accept"] = "text/turtle"
|
|
135
|
+
|
|
136
|
+
response = http.request(request)
|
|
137
|
+
|
|
138
|
+
case response.code
|
|
139
|
+
when "200"
|
|
140
|
+
return response.body
|
|
141
|
+
when "301", "302", "303", "307", "308"
|
|
142
|
+
redirects += 1
|
|
143
|
+
if redirects > max_redirects
|
|
144
|
+
raise "Too many redirects for #{url}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
location = response["location"]
|
|
148
|
+
if location.nil?
|
|
149
|
+
raise "Redirect response missing location header for #{url}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
uri = URI(location)
|
|
153
|
+
next
|
|
154
|
+
else
|
|
155
|
+
raise "Failed to download #{url}: #{response.code} #{response.message}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Parse TTL content into RDF graph
|
|
162
|
+
def parse_ttl_content(ttl_content)
|
|
163
|
+
graph = RDF::Graph.new
|
|
164
|
+
RDF::Turtle::Reader.new(ttl_content) do |reader|
|
|
165
|
+
reader.each_statement do |statement|
|
|
166
|
+
graph << statement
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
graph
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Extract entities from RDF graph
|
|
173
|
+
def extract_entities(graph, vocab_type, base_url)
|
|
174
|
+
entities = []
|
|
175
|
+
|
|
176
|
+
# Find all subjects that are instances of the vocabulary type
|
|
177
|
+
subjects = find_vocabulary_subjects(graph, vocab_type, base_url)
|
|
178
|
+
|
|
179
|
+
subjects.each do |subject|
|
|
180
|
+
entity = create_entity(graph, subject, vocab_type)
|
|
181
|
+
entities << entity if entity
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
entities
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Find subjects that belong to the vocabulary
|
|
188
|
+
def find_vocabulary_subjects(graph, _vocab_type, base_url)
|
|
189
|
+
subjects = Set.new
|
|
190
|
+
|
|
191
|
+
# Get all subjects that have properties from this vocabulary
|
|
192
|
+
graph.each_statement do |statement|
|
|
193
|
+
subject_uri = statement.subject.to_s
|
|
194
|
+
|
|
195
|
+
# Check if subject URI starts with the vocabulary base URL
|
|
196
|
+
if subject_uri.start_with?(base_url.sub("/3.1.2/vocab/",
|
|
197
|
+
"/vocab/"))
|
|
198
|
+
subjects << statement.subject
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
subjects.to_a
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Create entity object from RDF data
|
|
206
|
+
def create_entity(graph, subject, vocab_type)
|
|
207
|
+
case vocab_type
|
|
208
|
+
when :units
|
|
209
|
+
create_unit(graph, subject)
|
|
210
|
+
when :quantitykinds
|
|
211
|
+
create_quantity_kind(graph, subject)
|
|
212
|
+
when :dimensionvectors
|
|
213
|
+
create_dimension_vector(graph, subject)
|
|
214
|
+
when :sou
|
|
215
|
+
create_system_of_units(graph, subject)
|
|
216
|
+
when :prefixes
|
|
217
|
+
create_prefix(graph, subject)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Create QudtUnit from RDF data
|
|
222
|
+
def create_unit(graph, subject)
|
|
223
|
+
unit = QudtUnit.new
|
|
224
|
+
unit.uri = subject.to_s
|
|
225
|
+
|
|
226
|
+
graph.query([subject, nil, nil]) do |statement|
|
|
227
|
+
predicate = statement.predicate
|
|
228
|
+
object = statement.object
|
|
229
|
+
|
|
230
|
+
case predicate
|
|
231
|
+
when QUDT_PREDICATES[:label]
|
|
232
|
+
unit.label = object.to_s
|
|
233
|
+
when QUDT_PREDICATES[:symbol]
|
|
234
|
+
unit.symbol = object.to_s
|
|
235
|
+
when QUDT_PREDICATES[:has_quantity_kind]
|
|
236
|
+
unit.has_quantity_kind = object.to_s
|
|
237
|
+
when QUDT_PREDICATES[:has_dimension_vector]
|
|
238
|
+
unit.has_dimension_vector = object.to_s
|
|
239
|
+
when QUDT_PREDICATES[:conversion_multiplier]
|
|
240
|
+
unit.conversion_multiplier = convert_to_float(object)
|
|
241
|
+
when QUDT_PREDICATES[:conversion_offset]
|
|
242
|
+
unit.conversion_offset = convert_to_float(object)
|
|
243
|
+
when QUDT_PREDICATES[:description]
|
|
244
|
+
unit.description = object.to_s
|
|
245
|
+
when QUDT_PREDICATES[:si_exact_match]
|
|
246
|
+
unit.si_exact_match = object.to_s
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
unit
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Create QudtQuantityKind from RDF data
|
|
254
|
+
def create_quantity_kind(graph, subject)
|
|
255
|
+
quantity_kind = QudtQuantityKind.new
|
|
256
|
+
quantity_kind.uri = subject.to_s
|
|
257
|
+
|
|
258
|
+
graph.query([subject, nil, nil]) do |statement|
|
|
259
|
+
predicate = statement.predicate
|
|
260
|
+
object = statement.object
|
|
261
|
+
|
|
262
|
+
case predicate
|
|
263
|
+
when QUDT_PREDICATES[:label]
|
|
264
|
+
quantity_kind.label = object.to_s
|
|
265
|
+
when QUDT_PREDICATES[:symbol]
|
|
266
|
+
quantity_kind.symbol = object.to_s
|
|
267
|
+
when QUDT_PREDICATES[:has_dimension_vector]
|
|
268
|
+
quantity_kind.has_dimension_vector = object.to_s
|
|
269
|
+
when QUDT_PREDICATES[:description]
|
|
270
|
+
quantity_kind.description = object.to_s
|
|
271
|
+
when QUDT_PREDICATES[:si_exact_match]
|
|
272
|
+
quantity_kind.si_exact_match = object.to_s
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
quantity_kind
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Create QudtDimensionVector from RDF data
|
|
280
|
+
def create_dimension_vector(graph, subject)
|
|
281
|
+
dimension_vector = QudtDimensionVector.new
|
|
282
|
+
dimension_vector.uri = subject.to_s
|
|
283
|
+
|
|
284
|
+
graph.query([subject, nil, nil]) do |statement|
|
|
285
|
+
predicate = statement.predicate
|
|
286
|
+
object = statement.object
|
|
287
|
+
|
|
288
|
+
case predicate
|
|
289
|
+
when QUDT_PREDICATES[:label]
|
|
290
|
+
dimension_vector.label = object.to_s
|
|
291
|
+
when QUDT_PREDICATES[:description]
|
|
292
|
+
dimension_vector.description = object.to_s
|
|
293
|
+
when QUDT_PREDICATES[:dimension_exponent_for_length]
|
|
294
|
+
dimension_vector.dimension_exponent_for_length = convert_to_integer(object)
|
|
295
|
+
when QUDT_PREDICATES[:dimension_exponent_for_mass]
|
|
296
|
+
dimension_vector.dimension_exponent_for_mass = convert_to_integer(object)
|
|
297
|
+
when QUDT_PREDICATES[:dimension_exponent_for_time]
|
|
298
|
+
dimension_vector.dimension_exponent_for_time = convert_to_integer(object)
|
|
299
|
+
when QUDT_PREDICATES[:dimension_exponent_for_electric_current]
|
|
300
|
+
dimension_vector.dimension_exponent_for_electric_current = convert_to_integer(object)
|
|
301
|
+
when QUDT_PREDICATES[:dimension_exponent_for_thermodynamic_temperature]
|
|
302
|
+
dimension_vector.dimension_exponent_for_thermodynamic_temperature = convert_to_integer(object)
|
|
303
|
+
when QUDT_PREDICATES[:dimension_exponent_for_amount_of_substance]
|
|
304
|
+
dimension_vector.dimension_exponent_for_amount_of_substance = convert_to_integer(object)
|
|
305
|
+
when QUDT_PREDICATES[:dimension_exponent_for_luminous_intensity]
|
|
306
|
+
dimension_vector.dimension_exponent_for_luminous_intensity = convert_to_integer(object)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
dimension_vector
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Create QudtSystemOfUnits from RDF data
|
|
314
|
+
def create_system_of_units(graph, subject)
|
|
315
|
+
system = QudtSystemOfUnits.new
|
|
316
|
+
system.uri = subject.to_s
|
|
317
|
+
|
|
318
|
+
graph.query([subject, nil, nil]) do |statement|
|
|
319
|
+
predicate = statement.predicate
|
|
320
|
+
object = statement.object
|
|
321
|
+
|
|
322
|
+
case predicate
|
|
323
|
+
when QUDT_PREDICATES[:label]
|
|
324
|
+
system.label = object.to_s
|
|
325
|
+
when QUDT_PREDICATES[:abbreviation]
|
|
326
|
+
system.abbreviation = object.to_s
|
|
327
|
+
when QUDT_PREDICATES[:description]
|
|
328
|
+
system.description = object.to_s
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
system
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Create QudtPrefix from RDF data
|
|
336
|
+
def create_prefix(graph, subject)
|
|
337
|
+
prefix = QudtPrefix.new
|
|
338
|
+
prefix.uri = subject.to_s
|
|
339
|
+
|
|
340
|
+
# Determine prefix type from RDF type
|
|
341
|
+
graph.query([subject, QUDT_PREDICATES[:rdf_type],
|
|
342
|
+
nil]) do |statement|
|
|
343
|
+
type_uri = statement.object.to_s
|
|
344
|
+
if type_uri.include?("DecimalPrefix")
|
|
345
|
+
prefix.prefix_type = "DecimalPrefix"
|
|
346
|
+
elsif type_uri.include?("BinaryPrefix")
|
|
347
|
+
prefix.prefix_type = "BinaryPrefix"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
graph.query([subject, nil, nil]) do |statement|
|
|
352
|
+
predicate = statement.predicate
|
|
353
|
+
object = statement.object
|
|
354
|
+
|
|
355
|
+
case predicate
|
|
356
|
+
when QUDT_PREDICATES[:label]
|
|
357
|
+
prefix.label = object.to_s
|
|
358
|
+
when QUDT_PREDICATES[:symbol]
|
|
359
|
+
prefix.symbol = object.to_s
|
|
360
|
+
when QUDT_PREDICATES[:prefix_multiplier]
|
|
361
|
+
prefix.prefix_multiplier = convert_to_float(object)
|
|
362
|
+
when QUDT_PREDICATES[:prefix_multiplier_sn]
|
|
363
|
+
prefix.prefix_multiplier_sn = object.to_s
|
|
364
|
+
when QUDT_PREDICATES[:ucum_code]
|
|
365
|
+
prefix.ucum_code = object.to_s
|
|
366
|
+
when QUDT_PREDICATES[:si_exact_match]
|
|
367
|
+
prefix.si_exact_match = object.to_s
|
|
368
|
+
when QUDT_PREDICATES[:description], QUDT_PREDICATES[:dc_description]
|
|
369
|
+
prefix.description = object.to_s
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
prefix
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Convert RDF object to float, handling RDF::Literal objects
|
|
377
|
+
def convert_to_float(object)
|
|
378
|
+
case object
|
|
379
|
+
when RDF::Literal
|
|
380
|
+
object.object.to_f
|
|
381
|
+
else
|
|
382
|
+
object.to_s.to_f
|
|
383
|
+
end
|
|
384
|
+
rescue StandardError
|
|
385
|
+
0.0
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Convert RDF object to integer, handling RDF::Literal objects
|
|
389
|
+
def convert_to_integer(object)
|
|
390
|
+
case object
|
|
391
|
+
when RDF::Literal
|
|
392
|
+
object.object.to_i
|
|
393
|
+
else
|
|
394
|
+
object.to_s.to_i
|
|
395
|
+
end
|
|
396
|
+
rescue StandardError
|
|
397
|
+
0
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Unitsdb
|
|
6
|
+
module Commands
|
|
7
|
+
module Qudt
|
|
8
|
+
class Update < Base
|
|
9
|
+
# Constants
|
|
10
|
+
ENTITY_TYPES = %w[units quantities dimensions unit_systems
|
|
11
|
+
prefixes].freeze
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
# Get options
|
|
15
|
+
entity_type = @options[:entity_type]&.downcase
|
|
16
|
+
output_dir = @options[:output_dir]
|
|
17
|
+
include_potential = @options[:include_potential_matches] || false
|
|
18
|
+
database_path = @options[:database]
|
|
19
|
+
ttl_dir = @options[:ttl_dir]
|
|
20
|
+
source_type = ttl_dir ? :file : :url
|
|
21
|
+
|
|
22
|
+
# Validate parameters
|
|
23
|
+
validate_parameters(ttl_dir, source_type)
|
|
24
|
+
|
|
25
|
+
# Use the path as-is without expansion
|
|
26
|
+
puts "Using database directory: #{database_path}"
|
|
27
|
+
|
|
28
|
+
@db = Unitsdb::Database.from_db(database_path)
|
|
29
|
+
|
|
30
|
+
# Set output directory to database path if not specified
|
|
31
|
+
output_dir ||= database_path
|
|
32
|
+
|
|
33
|
+
if source_type == :file
|
|
34
|
+
puts "Using QUDT TTL directory: #{ttl_dir}"
|
|
35
|
+
else
|
|
36
|
+
puts "Downloading QUDT vocabularies from online sources"
|
|
37
|
+
end
|
|
38
|
+
puts "Output directory: #{output_dir}"
|
|
39
|
+
puts "Include potential matches: #{include_potential ? 'Yes' : 'No'}"
|
|
40
|
+
|
|
41
|
+
# Parse QUDT vocabularies
|
|
42
|
+
qudt_data = TtlParser.parse_qudt_vocabularies(
|
|
43
|
+
source_type: source_type, ttl_dir: ttl_dir,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Process entity types
|
|
47
|
+
process_entities(entity_type, qudt_data, output_dir,
|
|
48
|
+
include_potential)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Process all entity types or a specific one
|
|
54
|
+
def process_entities(entity_type, qudt_data, output_dir,
|
|
55
|
+
include_potential)
|
|
56
|
+
if entity_type && ENTITY_TYPES.include?(entity_type)
|
|
57
|
+
process_entity_type(entity_type, qudt_data, output_dir,
|
|
58
|
+
include_potential)
|
|
59
|
+
else
|
|
60
|
+
ENTITY_TYPES.each do |type|
|
|
61
|
+
process_entity_type(type, qudt_data, output_dir,
|
|
62
|
+
include_potential)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Process a specific entity type
|
|
68
|
+
def process_entity_type(entity_type, qudt_data, output_dir,
|
|
69
|
+
include_potential = false)
|
|
70
|
+
puts "\n========== Updating #{entity_type.upcase} References ==========\n"
|
|
71
|
+
|
|
72
|
+
# Get entities from the specific YAML file (like UCUM does)
|
|
73
|
+
class_name = case entity_type
|
|
74
|
+
when "units" then "Units"
|
|
75
|
+
when "quantities" then "Quantities"
|
|
76
|
+
when "dimensions" then "Dimensions"
|
|
77
|
+
when "unit_systems" then "UnitSystems"
|
|
78
|
+
when "prefixes" then "Prefixes"
|
|
79
|
+
else entity_type.capitalize
|
|
80
|
+
end
|
|
81
|
+
klass = Unitsdb.const_get(class_name)
|
|
82
|
+
yaml_path = File.join(@options[:database], "#{entity_type}.yaml")
|
|
83
|
+
entity_collection = klass.from_yaml(File.read(yaml_path))
|
|
84
|
+
|
|
85
|
+
qudt_entities = TtlParser.get_entities_from_qudt(entity_type,
|
|
86
|
+
qudt_data)
|
|
87
|
+
|
|
88
|
+
puts "Found #{qudt_entities.size} #{entity_type} in QUDT"
|
|
89
|
+
puts "Found #{entity_collection.send(entity_type).size} #{entity_type} in database"
|
|
90
|
+
|
|
91
|
+
# Match UnitsDB entities to QUDT entities
|
|
92
|
+
matches, missing_refs, unmatched_db = Matcher.match_db_to_qudt(
|
|
93
|
+
entity_type, qudt_entities, entity_collection.send(entity_type)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
puts "Matched: #{matches.size}"
|
|
97
|
+
puts "Missing references (will be added): #{missing_refs.size}"
|
|
98
|
+
puts "Unmatched UnitsDB entities: #{unmatched_db.size}"
|
|
99
|
+
|
|
100
|
+
# Update references if there are missing references
|
|
101
|
+
if missing_refs.empty?
|
|
102
|
+
puts "No new QUDT references to add for #{entity_type}"
|
|
103
|
+
else
|
|
104
|
+
# Create output directory if it doesn't exist
|
|
105
|
+
FileUtils.mkdir_p(output_dir)
|
|
106
|
+
|
|
107
|
+
output_file = File.join(output_dir, "#{entity_type}.yaml")
|
|
108
|
+
# Set environment variable so updater can find the original file
|
|
109
|
+
ENV["UNITSDB_DATABASE_PATH"] = @options[:database]
|
|
110
|
+
Updater.update_references(entity_type, missing_refs,
|
|
111
|
+
entity_collection, output_file, include_potential)
|
|
112
|
+
puts "Updated references written to #{output_file}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Validation helpers
|
|
117
|
+
def validate_parameters(ttl_dir, source_type)
|
|
118
|
+
return unless source_type == :file && ttl_dir && !Dir.exist?(ttl_dir)
|
|
119
|
+
|
|
120
|
+
raise Unitsdb::Errors::FileNotFoundError,
|
|
121
|
+
"TTL directory not found: #{ttl_dir}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|