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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +8 -1
  3. data/.gitignore +2 -0
  4. data/.gitmodules +4 -3
  5. data/.rubocop.yml +13 -8
  6. data/.rubocop_todo.yml +217 -100
  7. data/CLAUDE.md +55 -0
  8. data/Gemfile +4 -1
  9. data/README.adoc +283 -16
  10. data/data/dimensions.yaml +1864 -0
  11. data/data/prefixes.yaml +874 -0
  12. data/data/quantities.yaml +3715 -0
  13. data/data/scales.yaml +97 -0
  14. data/data/schemas/dimensions-schema.yaml +153 -0
  15. data/data/schemas/prefixes-schema.yaml +155 -0
  16. data/data/schemas/quantities-schema.yaml +117 -0
  17. data/data/schemas/scales-schema.yaml +106 -0
  18. data/data/schemas/unit_systems-schema.yaml +116 -0
  19. data/data/schemas/units-schema.yaml +215 -0
  20. data/data/unit_systems.yaml +78 -0
  21. data/data/units.yaml +14052 -0
  22. data/exe/unitsdb +7 -1
  23. data/lib/unitsdb/cli.rb +42 -15
  24. data/lib/unitsdb/commands/_modify.rb +40 -4
  25. data/lib/unitsdb/commands/base.rb +6 -2
  26. data/lib/unitsdb/commands/check_si/si_formatter.rb +488 -0
  27. data/lib/unitsdb/commands/check_si/si_matcher.rb +487 -0
  28. data/lib/unitsdb/commands/check_si/si_ttl_parser.rb +103 -0
  29. data/lib/unitsdb/commands/check_si/si_updater.rb +254 -0
  30. data/lib/unitsdb/commands/check_si.rb +54 -35
  31. data/lib/unitsdb/commands/get.rb +11 -10
  32. data/lib/unitsdb/commands/normalize.rb +21 -7
  33. data/lib/unitsdb/commands/qudt/check.rb +150 -0
  34. data/lib/unitsdb/commands/qudt/formatter.rb +194 -0
  35. data/lib/unitsdb/commands/qudt/matcher.rb +746 -0
  36. data/lib/unitsdb/commands/qudt/ttl_parser.rb +403 -0
  37. data/lib/unitsdb/commands/qudt/update.rb +126 -0
  38. data/lib/unitsdb/commands/qudt/updater.rb +189 -0
  39. data/lib/unitsdb/commands/qudt.rb +82 -0
  40. data/lib/unitsdb/commands/release.rb +12 -9
  41. data/lib/unitsdb/commands/search.rb +12 -11
  42. data/lib/unitsdb/commands/ucum/check.rb +42 -29
  43. data/lib/unitsdb/commands/ucum/formatter.rb +2 -1
  44. data/lib/unitsdb/commands/ucum/matcher.rb +23 -9
  45. data/lib/unitsdb/commands/ucum/update.rb +14 -13
  46. data/lib/unitsdb/commands/ucum/updater.rb +40 -6
  47. data/lib/unitsdb/commands/ucum/xml_parser.rb +0 -2
  48. data/lib/unitsdb/commands/ucum.rb +44 -4
  49. data/lib/unitsdb/commands/validate/identifiers.rb +2 -4
  50. data/lib/unitsdb/commands/validate/qudt_references.rb +111 -0
  51. data/lib/unitsdb/commands/validate/references.rb +36 -19
  52. data/lib/unitsdb/commands/validate/si_references.rb +3 -5
  53. data/lib/unitsdb/commands/validate/ucum_references.rb +105 -0
  54. data/lib/unitsdb/commands/validate.rb +67 -11
  55. data/lib/unitsdb/commands.rb +20 -0
  56. data/lib/unitsdb/database.rb +90 -52
  57. data/lib/unitsdb/dimension.rb +1 -4
  58. data/lib/unitsdb/dimension_details.rb +0 -1
  59. data/lib/unitsdb/dimensions.rb +0 -2
  60. data/lib/unitsdb/errors.rb +7 -0
  61. data/lib/unitsdb/prefix.rb +0 -4
  62. data/lib/unitsdb/prefix_reference.rb +0 -2
  63. data/lib/unitsdb/prefixes.rb +0 -1
  64. data/lib/unitsdb/quantities.rb +0 -2
  65. data/lib/unitsdb/quantity.rb +0 -6
  66. data/lib/unitsdb/qudt.rb +100 -0
  67. data/lib/unitsdb/root_unit_reference.rb +0 -3
  68. data/lib/unitsdb/scale.rb +0 -4
  69. data/lib/unitsdb/scale_reference.rb +0 -2
  70. data/lib/unitsdb/scales.rb +0 -2
  71. data/lib/unitsdb/si_derived_base.rb +0 -2
  72. data/lib/unitsdb/ucum.rb +14 -10
  73. data/lib/unitsdb/unit.rb +0 -10
  74. data/lib/unitsdb/unit_reference.rb +0 -2
  75. data/lib/unitsdb/unit_system.rb +1 -3
  76. data/lib/unitsdb/unit_system_reference.rb +0 -2
  77. data/lib/unitsdb/unit_systems.rb +0 -2
  78. data/lib/unitsdb/units.rb +0 -2
  79. data/lib/unitsdb/utils.rb +32 -21
  80. data/lib/unitsdb/version.rb +5 -1
  81. data/lib/unitsdb.rb +62 -14
  82. data/unitsdb.gemspec +6 -3
  83. metadata +52 -13
  84. data/lib/unitsdb/commands/si_formatter.rb +0 -485
  85. data/lib/unitsdb/commands/si_matcher.rb +0 -470
  86. data/lib/unitsdb/commands/si_ttl_parser.rb +0 -100
  87. 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