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,746 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ module Commands
5
+ module Qudt
6
+ # Matcher for QUDT and UnitsDB entities
7
+ module Matcher
8
+ module_function
9
+
10
+ # Match QUDT entities to UnitsDB entities (QUDT → UnitsDB)
11
+ def match_qudt_to_db(entity_type, qudt_entities, db_entities)
12
+ puts "Matching QUDT #{entity_type} to UnitsDB #{entity_type}..."
13
+
14
+ # Initialize result arrays
15
+ matches = []
16
+ missing_matches = []
17
+ unmatched_qudt = []
18
+
19
+ # Process each QUDT entity
20
+ qudt_entities.each do |qudt_entity|
21
+ match_data = find_db_match_for_qudt(qudt_entity, db_entities,
22
+ entity_type)
23
+
24
+ if match_data[:match]
25
+ matches << { qudt_entity: qudt_entity,
26
+ db_entity: match_data[:match] }
27
+ elsif match_data[:potential_match]
28
+ missing_matches << { qudt_entity: qudt_entity,
29
+ db_entity: match_data[:potential_match] }
30
+ else
31
+ unmatched_qudt << qudt_entity
32
+ end
33
+ end
34
+
35
+ [matches, missing_matches, unmatched_qudt]
36
+ end
37
+
38
+ # Match UnitsDB entities to QUDT entities (UnitsDB → QUDT)
39
+ def match_db_to_qudt(entity_type, qudt_entities, db_entities)
40
+ puts "Matching UnitsDB #{entity_type} to QUDT #{entity_type}..."
41
+
42
+ # Initialize result arrays
43
+ matches = []
44
+ missing_refs = []
45
+ unmatched_db = []
46
+
47
+ # Process each UnitsDB entity
48
+ db_entities.each do |db_entity|
49
+ # Skip entities that already have QUDT references
50
+ if has_qudt_reference?(db_entity)
51
+ matches << { db_entity: db_entity,
52
+ qudt_entity: find_referenced_qudt_entity(db_entity,
53
+ qudt_entities) }
54
+ next
55
+ end
56
+
57
+ match_data = find_qudt_match_for_db(db_entity, qudt_entities,
58
+ entity_type)
59
+
60
+ if match_data[:match]
61
+ missing_refs << { db_entity: db_entity,
62
+ qudt_entity: match_data[:match] }
63
+ else
64
+ unmatched_db << db_entity
65
+ end
66
+ end
67
+
68
+ [matches, missing_refs, unmatched_db]
69
+ end
70
+
71
+ # Check if a UnitsDB entity already has a QUDT reference
72
+ def has_qudt_reference?(entity)
73
+ return false unless entity.respond_to?(:references) && entity.references
74
+
75
+ entity.references.any? { |ref| ref.authority == "qudt" }
76
+ end
77
+
78
+ # Find the referenced QUDT entity based on the reference URI
79
+ def find_referenced_qudt_entity(db_entity, qudt_entities)
80
+ return nil unless db_entity.respond_to?(:references) && db_entity.references
81
+
82
+ qudt_ref = db_entity.references.find { |ref| ref.authority == "qudt" }
83
+ return nil unless qudt_ref
84
+
85
+ ref_uri = qudt_ref.uri
86
+ qudt_entities.find { |qudt_entity| qudt_entity.uri == ref_uri }
87
+ end
88
+
89
+ # Get the ID of a UnitsDB entity
90
+ def get_entity_id(entity)
91
+ entity.respond_to?(:id) ? entity.id : nil
92
+ end
93
+
94
+ # Find a matching UnitsDB entity for a QUDT entity
95
+ def find_db_match_for_qudt(qudt_entity, db_entities, entity_type)
96
+ result = { match: nil, potential_match: nil }
97
+
98
+ # Different matching logic based on entity type
99
+ case entity_type
100
+ when "units"
101
+ result = match_unit_qudt_to_db(qudt_entity, db_entities)
102
+ when "quantities"
103
+ result = match_quantity_qudt_to_db(qudt_entity, db_entities)
104
+ when "dimensions"
105
+ result = match_dimension_qudt_to_db(qudt_entity, db_entities)
106
+ when "unit_systems"
107
+ result = match_unit_system_qudt_to_db(qudt_entity, db_entities)
108
+ when "prefixes"
109
+ result = match_prefix_qudt_to_db(qudt_entity, db_entities)
110
+ end
111
+
112
+ result
113
+ end
114
+
115
+ # Find a matching QUDT entity for a UnitsDB entity
116
+ def find_qudt_match_for_db(db_entity, qudt_entities, entity_type)
117
+ result = { match: nil }
118
+
119
+ # Different matching logic based on entity type
120
+ case entity_type
121
+ when "units"
122
+ result = match_unit_db_to_qudt(db_entity, qudt_entities)
123
+ when "quantities"
124
+ result = match_quantity_db_to_qudt(db_entity, qudt_entities)
125
+ when "dimensions"
126
+ result = match_dimension_db_to_qudt(db_entity, qudt_entities)
127
+ when "unit_systems"
128
+ result = match_unit_system_db_to_qudt(db_entity, qudt_entities)
129
+ when "prefixes"
130
+ result = match_prefix_db_to_qudt(db_entity, qudt_entities)
131
+ end
132
+
133
+ result
134
+ end
135
+
136
+ # Match QUDT unit to UnitsDB unit
137
+ def match_unit_qudt_to_db(qudt_unit, db_units)
138
+ result = { match: nil, potential_match: nil }
139
+
140
+ # PRIORITY 0: Try SI exact match first (highest confidence)
141
+ if qudt_unit.si_exact_match
142
+ si_match = find_unit_by_si_reference(qudt_unit.si_exact_match,
143
+ db_units)
144
+ if si_match
145
+ result[:match] = si_match
146
+ return result
147
+ end
148
+ end
149
+
150
+ # PRIORITY 1: Try symbol match (most reliable)
151
+ if qudt_unit.symbol
152
+ symbol_match = db_units.find do |db_unit|
153
+ db_unit.symbols&.any? do |symbol|
154
+ symbol.ascii&.downcase == qudt_unit.symbol.downcase ||
155
+ symbol.unicode&.downcase == qudt_unit.symbol.downcase
156
+ end
157
+ end
158
+
159
+ if symbol_match
160
+ result[:match] = symbol_match
161
+ return result
162
+ end
163
+ end
164
+
165
+ # PRIORITY 2: Try exact label match
166
+ if qudt_unit.label
167
+ label_match = db_units.find do |db_unit|
168
+ db_unit.names&.any? do |name_obj|
169
+ name_obj.value.downcase == qudt_unit.label.downcase
170
+ end
171
+ end
172
+
173
+ if label_match
174
+ result[:match] = label_match
175
+ return result
176
+ end
177
+ end
178
+
179
+ # PRIORITY 3: Try normalized name matching (remove common variations)
180
+ if qudt_unit.label
181
+ normalized_match = db_units.find do |db_unit|
182
+ db_unit.names&.any? do |name_obj|
183
+ normalize_name(qudt_unit.label) == normalize_name(name_obj.value)
184
+ end
185
+ end
186
+
187
+ if normalized_match
188
+ result[:match] = normalized_match
189
+ return result
190
+ end
191
+ end
192
+
193
+ # PRIORITY 4: Try partial name match for potential matches (be conservative)
194
+ if qudt_unit.label && qudt_unit.label.length > 3
195
+ partial_matches = db_units.select do |db_unit|
196
+ db_unit.names&.any? do |name_obj|
197
+ name_obj.value.downcase.include?(qudt_unit.label.downcase) ||
198
+ qudt_unit.label.downcase.include?(name_obj.value.downcase)
199
+ end
200
+ end
201
+
202
+ if partial_matches.any?
203
+ result[:potential_match] =
204
+ partial_matches.first
205
+ end
206
+ end
207
+
208
+ result
209
+ end
210
+
211
+ # Match UnitsDB unit to QUDT unit
212
+ def match_unit_db_to_qudt(db_unit, qudt_units)
213
+ result = { match: nil }
214
+
215
+ # PRIORITY 1: Try symbol match first (most reliable)
216
+ if db_unit.symbols && !db_unit.symbols.empty?
217
+ symbol_match = qudt_units.find do |qudt_unit|
218
+ qudt_unit.symbol && db_unit.symbols.any? do |symbol|
219
+ qudt_unit.symbol.downcase == symbol.ascii&.downcase ||
220
+ qudt_unit.symbol.downcase == symbol.unicode&.downcase
221
+ end
222
+ end
223
+
224
+ if symbol_match
225
+ result[:match] = symbol_match
226
+ return result
227
+ end
228
+ end
229
+
230
+ # PRIORITY 2: Try exact name match
231
+ if db_unit.names && !db_unit.names.empty?
232
+ db_unit_names = db_unit.names.map do |name_obj|
233
+ name_obj.value.downcase
234
+ end
235
+
236
+ name_match = qudt_units.find do |qudt_unit|
237
+ qudt_unit.label && db_unit_names.include?(qudt_unit.label.downcase)
238
+ end
239
+
240
+ if name_match
241
+ result[:match] = name_match
242
+ return result
243
+ end
244
+
245
+ # PRIORITY 3: Try normalized name matching
246
+ normalized_match = qudt_units.find do |qudt_unit|
247
+ qudt_unit.label && db_unit_names.any? do |db_name|
248
+ normalize_name(qudt_unit.label) == normalize_name(db_name)
249
+ end
250
+ end
251
+
252
+ result[:match] = normalized_match if normalized_match
253
+ end
254
+
255
+ result
256
+ end
257
+
258
+ # Match QUDT quantity kind to UnitsDB quantity
259
+ def match_quantity_qudt_to_db(qudt_quantity, db_quantities)
260
+ result = { match: nil, potential_match: nil }
261
+
262
+ # Try exact label match first
263
+ if qudt_quantity.label
264
+ label_match = db_quantities.find do |db_quantity|
265
+ db_quantity.names&.any? do |name_obj|
266
+ name_obj.value.downcase == qudt_quantity.label.downcase
267
+ end
268
+ end
269
+
270
+ if label_match
271
+ result[:match] = label_match
272
+ return result
273
+ end
274
+ end
275
+
276
+ # Try symbol match if available
277
+ if qudt_quantity.symbol
278
+ symbol_match = db_quantities.find do |db_quantity|
279
+ db_quantity.names&.any? do |name_obj|
280
+ name_obj.value.downcase == qudt_quantity.symbol.downcase
281
+ end
282
+ end
283
+
284
+ if symbol_match
285
+ result[:match] = symbol_match
286
+ return result
287
+ end
288
+ end
289
+
290
+ # Try normalized name matching (remove common variations)
291
+ if qudt_quantity.label
292
+ normalized_match = db_quantities.find do |db_quantity|
293
+ db_quantity.names&.any? do |name_obj|
294
+ normalize_name(qudt_quantity.label) == normalize_name(name_obj.value)
295
+ end
296
+ end
297
+
298
+ if normalized_match
299
+ result[:match] = normalized_match
300
+ return result
301
+ end
302
+ end
303
+
304
+ # Try partial name match for potential matches
305
+ if qudt_quantity.label
306
+ partial_matches = db_quantities.select do |db_quantity|
307
+ db_quantity.names&.any? do |name_obj|
308
+ name_obj.value.downcase.include?(qudt_quantity.label.downcase) ||
309
+ qudt_quantity.label.downcase.include?(name_obj.value.downcase)
310
+ end
311
+ end
312
+
313
+ if partial_matches.any?
314
+ result[:potential_match] =
315
+ partial_matches.first
316
+ end
317
+ end
318
+
319
+ result
320
+ end
321
+
322
+ # Match UnitsDB quantity to QUDT quantity kind
323
+ def match_quantity_db_to_qudt(db_quantity, qudt_quantities)
324
+ result = { match: nil }
325
+
326
+ # Try name match first
327
+ if db_quantity.names && !db_quantity.names.empty?
328
+ db_quantity_names = db_quantity.names.map do |name_obj|
329
+ name_obj.value.downcase
330
+ end
331
+
332
+ name_match = qudt_quantities.find do |qudt_quantity|
333
+ qudt_quantity.label && db_quantity_names.include?(qudt_quantity.label.downcase)
334
+ end
335
+
336
+ if name_match
337
+ result[:match] = name_match
338
+ return result
339
+ end
340
+
341
+ # Try normalized name matching
342
+ normalized_match = qudt_quantities.find do |qudt_quantity|
343
+ qudt_quantity.label && db_quantity_names.any? do |db_name|
344
+ normalize_name(qudt_quantity.label) == normalize_name(db_name)
345
+ end
346
+ end
347
+
348
+ result[:match] = normalized_match if normalized_match
349
+ end
350
+
351
+ result
352
+ end
353
+
354
+ # Match QUDT dimension vector to UnitsDB dimension
355
+ def match_dimension_qudt_to_db(qudt_dimension, db_dimensions)
356
+ result = { match: nil, potential_match: nil }
357
+
358
+ # Try dimensional analysis match first (most reliable for dimension vectors)
359
+ dimensional_match = db_dimensions.find do |db_dimension|
360
+ dimensions_match?(qudt_dimension, db_dimension)
361
+ end
362
+
363
+ if dimensional_match
364
+ result[:match] = dimensional_match
365
+ return result
366
+ end
367
+
368
+ # Try exact label match as fallback
369
+ if qudt_dimension.label
370
+ label_match = db_dimensions.find do |db_dimension|
371
+ db_dimension.names&.any? do |name_obj|
372
+ name_obj.value.downcase == qudt_dimension.label.downcase
373
+ end
374
+ end
375
+
376
+ if label_match
377
+ result[:potential_match] = label_match
378
+ end
379
+ end
380
+
381
+ result
382
+ end
383
+
384
+ # Match UnitsDB dimension to QUDT dimension vector
385
+ def match_dimension_db_to_qudt(db_dimension, qudt_dimensions)
386
+ result = { match: nil }
387
+
388
+ # Try dimensional analysis match first (most reliable)
389
+ dimensional_match = qudt_dimensions.find do |qudt_dimension|
390
+ dimensions_match?(qudt_dimension, db_dimension)
391
+ end
392
+
393
+ if dimensional_match
394
+ result[:match] = dimensional_match
395
+ return result
396
+ end
397
+
398
+ # Try name match as fallback
399
+ if db_dimension.names && !db_dimension.names.empty?
400
+ db_dimension_names = db_dimension.names.map do |name_obj|
401
+ name_obj.value.downcase
402
+ end
403
+
404
+ name_match = qudt_dimensions.find do |qudt_dimension|
405
+ qudt_dimension.label && db_dimension_names.include?(qudt_dimension.label.downcase)
406
+ end
407
+
408
+ result[:match] = name_match if name_match
409
+ end
410
+
411
+ result
412
+ end
413
+
414
+ # Match QUDT system of units to UnitsDB unit system
415
+ def match_unit_system_qudt_to_db(qudt_system, db_unit_systems)
416
+ result = { match: nil, potential_match: nil }
417
+
418
+ # Try exact label match first
419
+ if qudt_system.label
420
+ label_match = db_unit_systems.find do |db_system|
421
+ db_system.names&.any? do |name_obj|
422
+ name_obj.value.downcase == qudt_system.label.downcase
423
+ end
424
+ end
425
+
426
+ if label_match
427
+ result[:match] = label_match
428
+ return result
429
+ end
430
+ end
431
+
432
+ # Try abbreviation match
433
+ if qudt_system.abbreviation
434
+ abbrev_match = db_unit_systems.find do |db_system|
435
+ db_system.names&.any? do |name_obj|
436
+ name_obj.value.downcase == qudt_system.abbreviation.downcase
437
+ end
438
+ end
439
+
440
+ if abbrev_match
441
+ result[:match] = abbrev_match
442
+ return result
443
+ end
444
+ end
445
+
446
+ # Try smart matching for known systems
447
+ if qudt_system.abbreviation
448
+ smart_match = db_unit_systems.find do |db_system|
449
+ db_system.names&.any? do |name_obj|
450
+ case qudt_system.abbreviation.downcase
451
+ when "si"
452
+ # Match SI abbreviation to any system containing "si"
453
+ name_obj.value.downcase.include?("si")
454
+ when "cgs"
455
+ # Match CGS abbreviation to any system containing "cgs"
456
+ name_obj.value.downcase.include?("cgs")
457
+ when "imperial"
458
+ # Match Imperial to any system containing "imperial"
459
+ name_obj.value.downcase.include?("imperial")
460
+ when "us customary"
461
+ # Match US Customary to any system containing "us" or "customary"
462
+ name_obj.value.downcase.include?("us") || name_obj.value.downcase.include?("customary")
463
+ else
464
+ false
465
+ end
466
+ end
467
+ end
468
+
469
+ if smart_match
470
+ result[:match] = smart_match
471
+ return result
472
+ end
473
+ end
474
+
475
+ result
476
+ end
477
+
478
+ # Match UnitsDB unit system to QUDT system of units
479
+ def match_unit_system_db_to_qudt(db_system, qudt_systems)
480
+ result = { match: nil }
481
+
482
+ # Try name match first
483
+ if db_system.names && !db_system.names.empty?
484
+ db_system_names = db_system.names.map do |name_obj|
485
+ name_obj.value.downcase
486
+ end
487
+
488
+ name_match = qudt_systems.find do |qudt_system|
489
+ (qudt_system.label && db_system_names.include?(qudt_system.label.downcase)) ||
490
+ (qudt_system.abbreviation && db_system_names.include?(qudt_system.abbreviation.downcase))
491
+ end
492
+
493
+ if name_match
494
+ result[:match] = name_match
495
+ return result
496
+ end
497
+
498
+ # Try smart matching for known systems
499
+ smart_match = qudt_systems.find do |qudt_system|
500
+ db_system_names.any? do |db_name|
501
+ if db_name.include?("si") && qudt_system.abbreviation&.downcase == "si"
502
+ true
503
+ elsif db_name.include?("cgs") && qudt_system.abbreviation&.downcase == "cgs"
504
+ true
505
+ elsif db_name.include?("imperial") && qudt_system.abbreviation&.downcase == "imperial"
506
+ true
507
+ elsif (db_name.include?("us") || db_name.include?("customary")) && qudt_system.abbreviation&.downcase == "us customary"
508
+ true
509
+ else
510
+ false
511
+ end
512
+ end
513
+ end
514
+
515
+ result[:match] = smart_match if smart_match
516
+ end
517
+
518
+ result
519
+ end
520
+
521
+ # Check if QUDT dimension vector matches UnitsDB dimension
522
+ def dimensions_match?(qudt_dimension, db_dimension)
523
+ return false unless qudt_dimension.respond_to?(:dimension_exponent_for_length)
524
+
525
+ # Map QUDT dimension exponents to UnitsDB dimension structure
526
+ qudt_exponents = {
527
+ length: qudt_dimension.dimension_exponent_for_length || 0,
528
+ mass: qudt_dimension.dimension_exponent_for_mass || 0,
529
+ time: qudt_dimension.dimension_exponent_for_time || 0,
530
+ electric_current: qudt_dimension.dimension_exponent_for_electric_current || 0,
531
+ thermodynamic_temperature: qudt_dimension.dimension_exponent_for_thermodynamic_temperature || 0,
532
+ amount_of_substance: qudt_dimension.dimension_exponent_for_amount_of_substance || 0,
533
+ luminous_intensity: qudt_dimension.dimension_exponent_for_luminous_intensity || 0,
534
+ }
535
+
536
+ # Get UnitsDB dimension exponents from direct properties
537
+ db_exponents = {
538
+ length: get_dimension_power(db_dimension, :length),
539
+ mass: get_dimension_power(db_dimension, :mass),
540
+ time: get_dimension_power(db_dimension, :time),
541
+ electric_current: get_dimension_power(db_dimension,
542
+ :electric_current),
543
+ thermodynamic_temperature: get_dimension_power(db_dimension,
544
+ :thermodynamic_temperature),
545
+ amount_of_substance: get_dimension_power(db_dimension,
546
+ :amount_of_substance),
547
+ luminous_intensity: get_dimension_power(db_dimension,
548
+ :luminous_intensity),
549
+ }
550
+
551
+ # Compare all dimension exponents
552
+ qudt_exponents == db_exponents
553
+ end
554
+
555
+ # Get dimension power from UnitsDB dimension entity
556
+ def get_dimension_power(db_dimension, dimension_type)
557
+ return 0 unless db_dimension.respond_to?(dimension_type)
558
+
559
+ dimension_property = db_dimension.send(dimension_type)
560
+ return 0 unless dimension_property.respond_to?(:power)
561
+
562
+ dimension_property.power || 0
563
+ end
564
+
565
+ # Normalize names by removing common variations and punctuation
566
+ def normalize_name(name)
567
+ return "" unless name
568
+
569
+ name.downcase
570
+ .gsub(/\s+/, " ") # normalize whitespace
571
+ .gsub(/[-_]/, " ") # convert dashes/underscores to spaces
572
+ .gsub(/[()\\]/, "") # remove parentheses and brackets
573
+ .gsub(/\bof\b/, "") # remove "of"
574
+ .gsub(/\bper\b/, "/") # convert "per" to "/"
575
+ .strip
576
+ end
577
+
578
+ # Find a UnitsDB unit that has an SI reference matching the given SI URI
579
+ def find_unit_by_si_reference(si_uri, db_units)
580
+ return nil unless si_uri
581
+
582
+ # Extract the SI unit identifier from the URI
583
+ # Example: "http://qudt.org/vocab/unit/M" -> "M"
584
+ si_identifier = si_uri.split("/").last
585
+
586
+ # Look for a UnitsDB unit that has an SI reference with this identifier
587
+ db_units.find do |db_unit|
588
+ next unless db_unit.respond_to?(:references) && db_unit.references
589
+
590
+ db_unit.references.any? do |ref|
591
+ ref.authority == "si" && (
592
+ ref.uri&.end_with?(si_identifier) ||
593
+ ref.uri&.include?(si_identifier)
594
+ )
595
+ end
596
+ end
597
+ end
598
+
599
+ # Match QUDT prefix to UnitsDB prefix
600
+ def match_prefix_qudt_to_db(qudt_prefix, db_prefixes)
601
+ result = { match: nil, potential_match: nil }
602
+
603
+ # PRIORITY 1: Try UCUM code match first (most reliable for prefixes)
604
+ if qudt_prefix.ucum_code
605
+ ucum_match = db_prefixes.find do |db_prefix|
606
+ db_prefix.respond_to?(:references) && db_prefix.references&.any? do |ref|
607
+ ref.authority == "ucum" && ref.uri&.include?(qudt_prefix.ucum_code)
608
+ end
609
+ end
610
+
611
+ if ucum_match
612
+ result[:match] = ucum_match
613
+ return result
614
+ end
615
+ end
616
+
617
+ # PRIORITY 2: Try symbol match (very reliable for prefixes)
618
+ if qudt_prefix.symbol
619
+ symbol_match = db_prefixes.find do |db_prefix|
620
+ db_prefix.symbols&.any? do |symbol|
621
+ symbol.ascii&.downcase == qudt_prefix.symbol.downcase ||
622
+ symbol.unicode&.downcase == qudt_prefix.symbol.downcase
623
+ end
624
+ end
625
+
626
+ if symbol_match
627
+ result[:match] = symbol_match
628
+ return result
629
+ end
630
+ end
631
+
632
+ # PRIORITY 3: Try exact label match
633
+ if qudt_prefix.label
634
+ label_match = db_prefixes.find do |db_prefix|
635
+ db_prefix.names&.any? do |name_obj|
636
+ name_obj.value.downcase == qudt_prefix.label.downcase
637
+ end
638
+ end
639
+
640
+ if label_match
641
+ result[:match] = label_match
642
+ return result
643
+ end
644
+ end
645
+
646
+ # PRIORITY 4: Try multiplier match (for prefixes with same scale factor)
647
+ if qudt_prefix.prefix_multiplier
648
+ multiplier_match = db_prefixes.find do |db_prefix|
649
+ db_prefix.respond_to?(:factor) &&
650
+ (db_prefix.factor - qudt_prefix.prefix_multiplier).abs < 1e-10
651
+ end
652
+
653
+ if multiplier_match
654
+ result[:match] = multiplier_match
655
+ return result
656
+ end
657
+ end
658
+
659
+ # PRIORITY 5: Try normalized name matching
660
+ if qudt_prefix.label
661
+ normalized_match = db_prefixes.find do |db_prefix|
662
+ db_prefix.names&.any? do |name_obj|
663
+ normalize_name(qudt_prefix.label) == normalize_name(name_obj.value)
664
+ end
665
+ end
666
+
667
+ if normalized_match
668
+ result[:potential_match] = normalized_match
669
+ end
670
+ end
671
+
672
+ result
673
+ end
674
+
675
+ # Match UnitsDB prefix to QUDT prefix
676
+ def match_prefix_db_to_qudt(db_prefix, qudt_prefixes)
677
+ result = { match: nil }
678
+
679
+ # PRIORITY 1: Try symbol match first (most reliable)
680
+ if db_prefix.symbols && !db_prefix.symbols.empty?
681
+ symbol_match = qudt_prefixes.find do |qudt_prefix|
682
+ qudt_prefix.symbol && db_prefix.symbols.any? do |symbol|
683
+ qudt_prefix.symbol.downcase == symbol.ascii&.downcase ||
684
+ qudt_prefix.symbol.downcase == symbol.unicode&.downcase
685
+ end
686
+ end
687
+
688
+ if symbol_match
689
+ result[:match] = symbol_match
690
+ return result
691
+ end
692
+ end
693
+
694
+ # PRIORITY 2: Try exact name match
695
+ if db_prefix.names && !db_prefix.names.empty?
696
+ db_prefix_names = db_prefix.names.map do |name_obj|
697
+ name_obj.value.downcase
698
+ end
699
+
700
+ name_match = qudt_prefixes.find do |qudt_prefix|
701
+ qudt_prefix.label && db_prefix_names.include?(qudt_prefix.label.downcase)
702
+ end
703
+
704
+ if name_match
705
+ result[:match] = name_match
706
+ return result
707
+ end
708
+
709
+ # PRIORITY 3: Try normalized name matching
710
+ normalized_match = qudt_prefixes.find do |qudt_prefix|
711
+ qudt_prefix.label && db_prefix_names.any? do |db_name|
712
+ normalize_name(qudt_prefix.label) == normalize_name(db_name)
713
+ end
714
+ end
715
+
716
+ if normalized_match
717
+ result[:match] = normalized_match
718
+ return result
719
+ end
720
+ end
721
+
722
+ # PRIORITY 4: Try multiplier match (for prefixes with same scale factor)
723
+ if db_prefix.respond_to?(:factor) && db_prefix.factor
724
+ multiplier_match = qudt_prefixes.find do |qudt_prefix|
725
+ qudt_prefix.prefix_multiplier &&
726
+ (qudt_prefix.prefix_multiplier - db_prefix.factor).abs < 1e-10
727
+ end
728
+
729
+ result[:match] = multiplier_match if multiplier_match
730
+ end
731
+
732
+ result
733
+ end
734
+
735
+ # Check if an entity has been manually verified (has a special flag)
736
+ def manually_verified?(entity)
737
+ return false unless entity.respond_to?(:references) && entity.references
738
+
739
+ entity.references.any? do |ref|
740
+ ref.authority == "qudt" && ref.respond_to?(:verified) && ref.verified
741
+ end
742
+ end
743
+ end
744
+ end
745
+ end
746
+ end