unitsdb 0.1.1 → 2.0.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +5 -0
  3. data/.github/workflows/depenedent-gems.yml +16 -0
  4. data/.gitmodules +3 -0
  5. data/.rspec +2 -1
  6. data/.rubocop_todo.yml +168 -15
  7. data/Gemfile +3 -2
  8. data/README.adoc +803 -1
  9. data/exe/unitsdb +7 -0
  10. data/lib/unitsdb/cli.rb +88 -0
  11. data/lib/unitsdb/commands/_modify.rb +22 -0
  12. data/lib/unitsdb/commands/base.rb +26 -0
  13. data/lib/unitsdb/commands/check_si.rb +124 -0
  14. data/lib/unitsdb/commands/get.rb +133 -0
  15. data/lib/unitsdb/commands/normalize.rb +81 -0
  16. data/lib/unitsdb/commands/release.rb +73 -0
  17. data/lib/unitsdb/commands/search.rb +219 -0
  18. data/lib/unitsdb/commands/si_formatter.rb +485 -0
  19. data/lib/unitsdb/commands/si_matcher.rb +470 -0
  20. data/lib/unitsdb/commands/si_ttl_parser.rb +100 -0
  21. data/lib/unitsdb/commands/si_updater.rb +212 -0
  22. data/lib/unitsdb/commands/ucum/check.rb +126 -0
  23. data/lib/unitsdb/commands/ucum/formatter.rb +141 -0
  24. data/lib/unitsdb/commands/ucum/matcher.rb +301 -0
  25. data/lib/unitsdb/commands/ucum/update.rb +84 -0
  26. data/lib/unitsdb/commands/ucum/updater.rb +98 -0
  27. data/lib/unitsdb/commands/ucum/xml_parser.rb +34 -0
  28. data/lib/unitsdb/commands/ucum.rb +43 -0
  29. data/lib/unitsdb/commands/validate/identifiers.rb +42 -0
  30. data/lib/unitsdb/commands/validate/references.rb +318 -0
  31. data/lib/unitsdb/commands/validate/si_references.rb +109 -0
  32. data/lib/unitsdb/commands/validate.rb +40 -0
  33. data/lib/unitsdb/config.rb +19 -0
  34. data/lib/unitsdb/database.rb +662 -0
  35. data/lib/unitsdb/dimension.rb +19 -25
  36. data/lib/unitsdb/dimension_details.rb +20 -0
  37. data/lib/unitsdb/dimension_reference.rb +8 -0
  38. data/lib/unitsdb/dimensions.rb +4 -6
  39. data/lib/unitsdb/errors.rb +13 -0
  40. data/lib/unitsdb/external_reference.rb +14 -0
  41. data/lib/unitsdb/identifier.rb +8 -0
  42. data/lib/unitsdb/localized_string.rb +17 -0
  43. data/lib/unitsdb/prefix.rb +11 -12
  44. data/lib/unitsdb/prefix_reference.rb +10 -0
  45. data/lib/unitsdb/prefixes.rb +4 -6
  46. data/lib/unitsdb/quantities.rb +4 -27
  47. data/lib/unitsdb/quantity.rb +12 -24
  48. data/lib/unitsdb/quantity_reference.rb +4 -7
  49. data/lib/unitsdb/root_unit_reference.rb +14 -0
  50. data/lib/unitsdb/scale.rb +17 -0
  51. data/lib/unitsdb/scale_properties.rb +12 -0
  52. data/lib/unitsdb/scale_reference.rb +10 -0
  53. data/lib/unitsdb/scales.rb +12 -0
  54. data/lib/unitsdb/si_derived_base.rb +13 -14
  55. data/lib/unitsdb/symbol_presentations.rb +14 -0
  56. data/lib/unitsdb/ucum.rb +198 -0
  57. data/lib/unitsdb/unit.rb +20 -26
  58. data/lib/unitsdb/unit_reference.rb +5 -8
  59. data/lib/unitsdb/unit_system.rb +8 -10
  60. data/lib/unitsdb/unit_system_reference.rb +10 -0
  61. data/lib/unitsdb/unit_systems.rb +4 -16
  62. data/lib/unitsdb/units.rb +4 -6
  63. data/lib/unitsdb/utils.rb +84 -0
  64. data/lib/unitsdb/version.rb +1 -1
  65. data/lib/unitsdb.rb +13 -10
  66. data/unitsdb.gemspec +6 -3
  67. metadata +120 -12
  68. data/lib/unitsdb/dimension_quantity.rb +0 -28
  69. data/lib/unitsdb/dimension_symbol.rb +0 -22
  70. data/lib/unitsdb/prefix_symbol.rb +0 -12
  71. data/lib/unitsdb/root_unit.rb +0 -17
  72. data/lib/unitsdb/root_units.rb +0 -20
  73. data/lib/unitsdb/symbol.rb +0 -17
  74. data/lib/unitsdb/unit_symbol.rb +0 -15
  75. data/lib/unitsdb/unitsdb.rb +0 -6
@@ -0,0 +1,662 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "unit"
4
+ require_relative "prefix"
5
+ require_relative "quantity"
6
+ require_relative "dimension"
7
+ require_relative "unit_system"
8
+ require_relative "errors"
9
+
10
+ module Unitsdb
11
+ class Database < Lutaml::Model::Serializable
12
+ # model Config.model_for(:units)
13
+
14
+ attribute :schema_version, :string
15
+ attribute :version, :string
16
+ attribute :units, Unit, collection: true
17
+ attribute :prefixes, Prefix, collection: true
18
+ attribute :quantities, Quantity, collection: true
19
+ attribute :dimensions, Dimension, collection: true
20
+ attribute :unit_systems, UnitSystem, collection: true
21
+
22
+ # Find an entity by its specific identifier and type
23
+ # @param id [String] the identifier value to search for
24
+ # @param type [String, Symbol] the entity type (units, prefixes, quantities, etc.)
25
+ # @return [Object, nil] the first entity with matching identifier or nil if not found
26
+ def find_by_type(id:, type:)
27
+ collection = send(type.to_s)
28
+ collection.find { |entity| entity.identifiers&.any? { |identifier| identifier.id == id } }
29
+ end
30
+
31
+ # Find an entity by its identifier id across all entity types
32
+ # @param id [String] the identifier value to search for
33
+ # @param type [String, nil] optional identifier type to match
34
+ # @return [Object, nil] the first entity with matching identifier or nil if not found
35
+ def get_by_id(id:, type: nil)
36
+ %w[units prefixes quantities dimensions unit_systems].each do |collection_name|
37
+ next unless respond_to?(collection_name)
38
+
39
+ collection = send(collection_name)
40
+ entity = collection.find do |e|
41
+ e.identifiers&.any? do |identifier|
42
+ identifier.id == id && (type.nil? || identifier.type == type)
43
+ end
44
+ end
45
+
46
+ return entity if entity
47
+ end
48
+
49
+ nil
50
+ end
51
+
52
+ # Search for entities containing the given text in identifiers, names, or short description
53
+ # @param params [Hash] search parameters
54
+ # @option params [String] :text The text to search for
55
+ # @option params [String, Symbol, nil] :type Optional entity type to limit search scope
56
+ # @return [Array] all entities matching the search criteria
57
+ def search(params = {})
58
+ text = params[:text]
59
+ type = params[:type]
60
+
61
+ return [] unless text
62
+
63
+ results = []
64
+
65
+ # Define which collections to search based on type parameter
66
+ collections = type ? [type.to_s] : %w[units prefixes quantities dimensions unit_systems]
67
+
68
+ collections.each do |collection_name|
69
+ next unless respond_to?(collection_name)
70
+
71
+ collection = send(collection_name)
72
+ collection.each do |entity|
73
+ # Search in identifiers
74
+ if entity.identifiers&.any? { |identifier| identifier.id.to_s.downcase.include?(text.downcase) }
75
+ results << entity
76
+ next
77
+ end
78
+
79
+ # Search in names (if the entity has names)
80
+ if entity.respond_to?(:names) && entity.names && entity.names.any? do |name|
81
+ name.value.to_s.downcase.include?(text.downcase)
82
+ end
83
+ results << entity
84
+ next
85
+ end
86
+
87
+ # Search in short description
88
+ if entity.respond_to?(:short) && entity.short &&
89
+ entity.short.to_s.downcase.include?(text.downcase)
90
+ results << entity
91
+ next
92
+ end
93
+
94
+ # Special case for prefix name (prefixes don't have names array)
95
+ next unless collection_name == "prefixes" && entity.respond_to?(:name) &&
96
+ entity.name.to_s.downcase.include?(text.downcase)
97
+
98
+ results << entity
99
+ next
100
+ end
101
+ end
102
+
103
+ results
104
+ end
105
+
106
+ # Find entities by symbol
107
+ # @param symbol [String] the symbol to search for (exact match, case-insensitive)
108
+ # @param entity_type [String, Symbol, nil] the entity type to search (units or prefixes)
109
+ # @return [Array] entities with matching symbol
110
+ def find_by_symbol(symbol, entity_type = nil)
111
+ return [] unless symbol
112
+
113
+ results = []
114
+
115
+ # Symbol search only applies to units and prefixes
116
+ collections = entity_type ? [entity_type.to_s] : %w[units prefixes]
117
+
118
+ collections.each do |collection_name|
119
+ next unless respond_to?(collection_name) && %w[units prefixes].include?(collection_name)
120
+
121
+ collection = send(collection_name)
122
+ collection.each do |entity|
123
+ if collection_name == "units" && entity.respond_to?(:symbols) && entity.symbols
124
+ # Units can have multiple symbols
125
+ matches = entity.symbols.any? do |sym|
126
+ sym.respond_to?(:ascii) && sym.ascii &&
127
+ sym.ascii.downcase == symbol.downcase
128
+ end
129
+
130
+ results << entity if matches
131
+ elsif collection_name == "prefixes" && entity.respond_to?(:symbols) && entity.symbols
132
+ # Prefixes have multiple symbols in 2.0.0
133
+ matches = entity.symbols.any? do |sym|
134
+ sym.respond_to?(:ascii) && sym.ascii &&
135
+ sym.ascii.downcase == symbol.downcase
136
+ end
137
+
138
+ results << entity if matches
139
+ end
140
+ end
141
+ end
142
+
143
+ results
144
+ end
145
+
146
+ # Match entities by name, short, or symbol with different match types
147
+ # @param params [Hash] match parameters
148
+ # @option params [String] :value The value to match against
149
+ # @option params [String, Symbol] :match_type The type of match to perform (exact, symbol)
150
+ # @option params [String, Symbol, nil] :entity_type Optional entity type to limit search scope
151
+ # @return [Hash] matches grouped by match type (exact, symbol_match) with match details
152
+ def match_entities(params = {})
153
+ value = params[:value]
154
+ match_type = params[:match_type]&.to_s || "exact"
155
+ entity_type = params[:entity_type]
156
+
157
+ return {} unless value
158
+
159
+ result = {
160
+ exact: [],
161
+ symbol_match: []
162
+ }
163
+
164
+ # Define collections to search based on entity_type parameter
165
+ collections = entity_type ? [entity_type.to_s] : %w[units prefixes quantities dimensions unit_systems]
166
+
167
+ collections.each do |collection_name|
168
+ next unless respond_to?(collection_name)
169
+
170
+ collection = send(collection_name)
171
+
172
+ collection.each do |entity|
173
+ # For exact matches - look at short and names
174
+ if %w[exact all].include?(match_type)
175
+ # Match by short
176
+ if entity.respond_to?(:short) && entity.short &&
177
+ entity.short.downcase == value.downcase
178
+ result[:exact] << {
179
+ entity: entity,
180
+ match_desc: "short_to_name",
181
+ details: "UnitsDB short '#{entity.short}' matches '#{value}'"
182
+ }
183
+ next
184
+ end
185
+
186
+ # Match by names
187
+ if entity.respond_to?(:names) && entity.names
188
+ matching_name = entity.names.find { |name| name.value.to_s.downcase == value.downcase }
189
+ if matching_name
190
+ result[:exact] << {
191
+ entity: entity,
192
+ match_desc: "name_to_name",
193
+ details: "UnitsDB name '#{matching_name.value}' (#{matching_name.lang}) matches '#{value}'"
194
+ }
195
+ next
196
+ end
197
+ end
198
+ end
199
+
200
+ # For symbol matches - only applicable to units and prefixes
201
+ if %w[symbol all].include?(match_type) &&
202
+ %w[units prefixes].include?(collection_name)
203
+ if collection_name == "units" && entity.respond_to?(:symbols) && entity.symbols
204
+ # Units can have multiple symbols
205
+ matching_symbol = entity.symbols.find do |sym|
206
+ sym.respond_to?(:ascii) && sym.ascii &&
207
+ sym.ascii.downcase == value.downcase
208
+ end
209
+
210
+ if matching_symbol
211
+ result[:symbol_match] << {
212
+ entity: entity,
213
+ match_desc: "symbol_match",
214
+ details: "UnitsDB symbol '#{matching_symbol.ascii}' matches '#{value}'"
215
+ }
216
+ end
217
+ elsif collection_name == "prefixes" && entity.respond_to?(:symbols) && entity.symbols
218
+ # Prefixes have multiple symbols in 2.0.0
219
+ matching_symbol = entity.symbols.find do |sym|
220
+ sym.respond_to?(:ascii) && sym.ascii &&
221
+ sym.ascii.downcase == value.downcase
222
+ end
223
+
224
+ if matching_symbol
225
+ result[:symbol_match] << {
226
+ entity: entity,
227
+ match_desc: "symbol_match",
228
+ details: "UnitsDB symbol '#{matching_symbol.ascii}' matches '#{value}'"
229
+ }
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ # Remove empty categories
237
+ result.delete_if { |_, v| v.empty? }
238
+
239
+ result
240
+ end
241
+
242
+ # Checks for uniqueness of identifiers and short names
243
+ def validate_uniqueness
244
+ results = {
245
+ short: {},
246
+ id: {}
247
+ }
248
+
249
+ # Validate short names for applicable collections
250
+ validate_shorts(units, "units", results)
251
+ validate_shorts(dimensions, "dimensions", results)
252
+ validate_shorts(unit_systems, "unit_systems", results)
253
+
254
+ # Validate identifiers for all collections
255
+ validate_identifiers(units, "units", results)
256
+ validate_identifiers(prefixes, "prefixes", results)
257
+ validate_identifiers(quantities, "quantities", results)
258
+ validate_identifiers(dimensions, "dimensions", results)
259
+ validate_identifiers(unit_systems, "unit_systems", results)
260
+
261
+ results
262
+ end
263
+
264
+ # Validates references between entities
265
+ def validate_references
266
+ invalid_refs = {}
267
+
268
+ # Build registry of all valid IDs first
269
+ registry = build_id_registry
270
+
271
+ # Check various reference types
272
+ check_dimension_references(registry, invalid_refs)
273
+ check_unit_system_references(registry, invalid_refs)
274
+ check_quantity_references(registry, invalid_refs)
275
+ check_root_unit_references(registry, invalid_refs)
276
+
277
+ invalid_refs
278
+ end
279
+
280
+ def self.from_db(dir_path)
281
+ # If dir_path is a relative path, make it relative to the current working directory
282
+ db_path = dir_path
283
+ puts "Database directory path: #{db_path}"
284
+
285
+ # Check if the directory exists
286
+ raise Errors::DatabaseNotFoundError, "Database directory not found: #{db_path}" unless Dir.exist?(db_path)
287
+
288
+ # Define required files
289
+ required_files = %w[prefixes.yaml dimensions.yaml units.yaml quantities.yaml unit_systems.yaml]
290
+ yaml_files = required_files.map { |file| File.join(dir_path, file) }
291
+
292
+ # Check if all required files exist
293
+ missing_files = required_files.reject { |file| File.exist?(File.join(dir_path, file)) }
294
+
295
+ if missing_files.any?
296
+ raise Errors::DatabaseFileNotFoundError,
297
+ "Missing required database files: #{missing_files.join(", ")}"
298
+ end
299
+
300
+ # Ensure we have path properly joined with filenames
301
+ prefixes_yaml = yaml_files[0]
302
+ dimensions_yaml = yaml_files[1]
303
+ units_yaml = yaml_files[2]
304
+ quantities_yaml = yaml_files[3]
305
+ unit_systems_yaml = yaml_files[4]
306
+
307
+ # Debug paths
308
+ if ENV["DEBUG"]
309
+ puts "[UnitsDB] Loading YAML files from directory: #{dir_path}"
310
+ puts " - #{prefixes_yaml}"
311
+ puts " - #{dimensions_yaml}"
312
+ puts " - #{units_yaml}"
313
+ puts " - #{quantities_yaml}"
314
+ puts " - #{unit_systems_yaml}"
315
+ end
316
+
317
+ # Load YAML files with better error handling
318
+ begin
319
+ prefixes_hash = YAML.safe_load(File.read(prefixes_yaml))
320
+ dimensions_hash = YAML.safe_load(File.read(dimensions_yaml))
321
+ units_hash = YAML.safe_load(File.read(units_yaml))
322
+ quantities_hash = YAML.safe_load(File.read(quantities_yaml))
323
+ unit_systems_hash = YAML.safe_load(File.read(unit_systems_yaml))
324
+ rescue Errno::ENOENT => e
325
+ raise Errors::DatabaseFileNotFoundError, "Failed to read database file: #{e.message}"
326
+ rescue Psych::SyntaxError => e
327
+ raise Errors::DatabaseFileInvalidError, "Invalid YAML in database file: #{e.message}"
328
+ rescue StandardError => e
329
+ raise Errors::DatabaseLoadError, "Error loading database: #{e.message}"
330
+ end
331
+
332
+ # Verify all files have schema_version field
333
+ missing_schema = []
334
+ missing_schema << "prefixes.yaml" unless prefixes_hash.key?("schema_version")
335
+ missing_schema << "dimensions.yaml" unless dimensions_hash.key?("schema_version")
336
+ missing_schema << "units.yaml" unless units_hash.key?("schema_version")
337
+ missing_schema << "quantities.yaml" unless quantities_hash.key?("schema_version")
338
+ missing_schema << "unit_systems.yaml" unless unit_systems_hash.key?("schema_version")
339
+
340
+ if missing_schema.any?
341
+ raise Errors::DatabaseFileInvalidError,
342
+ "Missing schema_version in files: #{missing_schema.join(", ")}"
343
+ end
344
+
345
+ # Extract versions from each file
346
+ prefixes_version = prefixes_hash["schema_version"]
347
+ dimensions_version = dimensions_hash["schema_version"]
348
+ units_version = units_hash["schema_version"]
349
+ quantities_version = quantities_hash["schema_version"]
350
+ unit_systems_version = unit_systems_hash["schema_version"]
351
+
352
+ # Check if all versions match
353
+ versions = [
354
+ prefixes_version,
355
+ dimensions_version,
356
+ units_version,
357
+ quantities_version,
358
+ unit_systems_version
359
+ ]
360
+
361
+ unless versions.uniq.size == 1
362
+ version_info = {
363
+ "prefixes.yaml" => prefixes_version,
364
+ "dimensions.yaml" => dimensions_version,
365
+ "units.yaml" => units_version,
366
+ "quantities.yaml" => quantities_version,
367
+ "unit_systems.yaml" => unit_systems_version
368
+ }
369
+ raise Errors::VersionMismatchError, "Version mismatch in database files: #{version_info.inspect}"
370
+ end
371
+
372
+ # Check if the version is supported
373
+ version = versions.first
374
+ unless version == "2.0.0"
375
+ raise Errors::UnsupportedVersionError,
376
+ "Unsupported database version: #{version}. Only version 2.0.0 is supported."
377
+ end
378
+
379
+ combined_yaml = {
380
+ "schema_version" => prefixes_version,
381
+ "prefixes" => prefixes_hash["prefixes"],
382
+ "dimensions" => dimensions_hash["dimensions"],
383
+ "units" => units_hash["units"],
384
+ "quantities" => quantities_hash["quantities"],
385
+ "unit_systems" => unit_systems_hash["unit_systems"]
386
+ }.to_yaml
387
+
388
+ from_yaml(combined_yaml)
389
+ end
390
+
391
+ private
392
+
393
+ # Helper methods for uniqueness validation
394
+ def validate_shorts(collection, type, results)
395
+ shorts = {}
396
+
397
+ collection.each_with_index do |item, index|
398
+ next unless item.respond_to?(:short) && item.short
399
+
400
+ (shorts[item.short] ||= []) << "index:#{index}"
401
+ end
402
+
403
+ # Add to results if duplicates found
404
+ shorts.each do |short, paths|
405
+ next unless paths.size > 1
406
+
407
+ (results[:short][type] ||= {})[short] = paths
408
+ end
409
+ end
410
+
411
+ def validate_identifiers(collection, type, results)
412
+ ids = {}
413
+
414
+ collection.each_with_index do |item, index|
415
+ next unless item.respond_to?(:identifiers)
416
+
417
+ # Process identifiers array for this item
418
+ item.identifiers.each_with_index do |identifier, id_index|
419
+ next unless identifier.respond_to?(:id) && identifier.id
420
+
421
+ id_key = identifier.id
422
+ loc = "index:#{index}:identifiers[#{id_index}]"
423
+ (ids[id_key] ||= []) << loc
424
+ end
425
+ end
426
+
427
+ # Add duplicates to results
428
+ ids.each do |id, paths|
429
+ unique_paths = paths.uniq
430
+ next unless unique_paths.size > 1
431
+
432
+ (results[:id][type] ||= {})[id] = unique_paths
433
+ end
434
+ end
435
+
436
+ # Helper methods for reference validation
437
+ def build_id_registry
438
+ registry = {}
439
+
440
+ # Add unit identifiers
441
+ registry["units"] = {}
442
+ units.each_with_index do |unit, index|
443
+ next unless unit.respond_to?(:identifiers)
444
+
445
+ unit.identifiers.each do |identifier|
446
+ next unless identifier.id && identifier.type
447
+
448
+ # Add composite key (type:id)
449
+ composite_key = "#{identifier.type}:#{identifier.id}"
450
+ registry["units"][composite_key] = "index:#{index}"
451
+
452
+ # Also add just the ID for backward compatibility
453
+ registry["units"][identifier.id] = "index:#{index}"
454
+ end
455
+ end
456
+
457
+ # Add dimension identifiers
458
+ registry["dimensions"] = {}
459
+ dimensions.each_with_index do |dimension, index|
460
+ next unless dimension.respond_to?(:identifiers)
461
+
462
+ dimension.identifiers.each do |identifier|
463
+ next unless identifier.id && identifier.type
464
+
465
+ composite_key = "#{identifier.type}:#{identifier.id}"
466
+ registry["dimensions"][composite_key] = "index:#{index}"
467
+ registry["dimensions"][identifier.id] = "index:#{index}"
468
+ end
469
+
470
+ # Also track dimensions by short name
471
+ if dimension.respond_to?(:short) && dimension.short
472
+ registry["dimensions_short"] ||= {}
473
+ registry["dimensions_short"][dimension.short] = "index:#{index}"
474
+ end
475
+ end
476
+
477
+ # Add quantity identifiers
478
+ registry["quantities"] = {}
479
+ quantities.each_with_index do |quantity, index|
480
+ next unless quantity.respond_to?(:identifiers)
481
+
482
+ quantity.identifiers.each do |identifier|
483
+ next unless identifier.id && identifier.type
484
+
485
+ composite_key = "#{identifier.type}:#{identifier.id}"
486
+ registry["quantities"][composite_key] = "index:#{index}"
487
+ registry["quantities"][identifier.id] = "index:#{index}"
488
+ end
489
+ end
490
+
491
+ # Add prefix identifiers
492
+ registry["prefixes"] = {}
493
+ prefixes.each_with_index do |prefix, index|
494
+ next unless prefix.respond_to?(:identifiers)
495
+
496
+ prefix.identifiers.each do |identifier|
497
+ next unless identifier.id && identifier.type
498
+
499
+ composite_key = "#{identifier.type}:#{identifier.id}"
500
+ registry["prefixes"][composite_key] = "index:#{index}"
501
+ registry["prefixes"][identifier.id] = "index:#{index}"
502
+ end
503
+ end
504
+
505
+ # Add unit system identifiers
506
+ registry["unit_systems"] = {}
507
+ unit_systems.each_with_index do |unit_system, index|
508
+ next unless unit_system.respond_to?(:identifiers)
509
+
510
+ unit_system.identifiers.each do |identifier|
511
+ next unless identifier.id && identifier.type
512
+
513
+ composite_key = "#{identifier.type}:#{identifier.id}"
514
+ registry["unit_systems"][composite_key] = "index:#{index}"
515
+ registry["unit_systems"][identifier.id] = "index:#{index}"
516
+ end
517
+
518
+ # Also track unit systems by short name
519
+ if unit_system.respond_to?(:short) && unit_system.short
520
+ registry["unit_systems_short"] ||= {}
521
+ registry["unit_systems_short"][unit_system.short] = "index:#{index}"
522
+ end
523
+ end
524
+
525
+ registry
526
+ end
527
+
528
+ def check_dimension_references(registry, invalid_refs)
529
+ dimensions.each_with_index do |dimension, index|
530
+ next unless dimension.respond_to?(:dimension_reference) && dimension.dimension_reference
531
+
532
+ ref_id = dimension.dimension_reference
533
+ ref_type = "dimensions"
534
+ ref_path = "dimensions:index:#{index}:dimension_reference"
535
+
536
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "dimensions")
537
+ end
538
+ end
539
+
540
+ def check_unit_system_references(registry, invalid_refs)
541
+ units.each_with_index do |unit, index|
542
+ next unless unit.respond_to?(:unit_system_reference) && unit.unit_system_reference
543
+
544
+ unit.unit_system_reference.each_with_index do |ref_id, idx|
545
+ ref_type = "unit_systems"
546
+ ref_path = "units:index:#{index}:unit_system_reference[#{idx}]"
547
+
548
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
549
+ end
550
+ end
551
+ end
552
+
553
+ def check_quantity_references(registry, invalid_refs)
554
+ units.each_with_index do |unit, index|
555
+ next unless unit.respond_to?(:quantity_references) && unit.quantity_references
556
+
557
+ unit.quantity_references.each_with_index do |ref_id, idx|
558
+ ref_type = "quantities"
559
+ ref_path = "units:index:#{index}:quantity_references[#{idx}]"
560
+
561
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
562
+ end
563
+ end
564
+ end
565
+
566
+ def check_root_unit_references(registry, invalid_refs)
567
+ units.each_with_index do |unit, index|
568
+ next unless unit.respond_to?(:root_units) && unit.root_units
569
+
570
+ unit.root_units.each_with_index do |root_unit, idx|
571
+ next unless root_unit.respond_to?(:unit_reference) && root_unit.unit_reference
572
+
573
+ # Check unit reference
574
+ ref_id = root_unit.unit_reference
575
+ ref_type = "units"
576
+ ref_path = "units:index:#{index}:root_units.#{idx}.unit_reference"
577
+
578
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
579
+
580
+ # Check prefix reference if present
581
+ next unless root_unit.respond_to?(:prefix_reference) && root_unit.prefix_reference
582
+
583
+ ref_id = root_unit.prefix_reference
584
+ ref_type = "prefixes"
585
+ ref_path = "units:index:#{index}:root_units.#{idx}.prefix_reference"
586
+
587
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
588
+ end
589
+ end
590
+ end
591
+
592
+ def validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, file_type)
593
+ # Handle references that are objects with id and type (could be a hash or an object)
594
+ if ref_id.respond_to?(:id) && ref_id.respond_to?(:type)
595
+ id = ref_id.id
596
+ type = ref_id.type
597
+ composite_key = "#{type}:#{id}"
598
+
599
+ # Try multiple lookup strategies
600
+ valid = false
601
+
602
+ # 1. Try exact composite key match
603
+ valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
604
+
605
+ # 2. Try just ID match if composite didn't work
606
+ valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
607
+
608
+ # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
609
+ if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
610
+ registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
611
+ registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
612
+ registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
613
+ )
614
+ # Special handling for unit_systems between unitsml and nist types
615
+ valid = true
616
+ end
617
+
618
+ unless valid
619
+ invalid_refs[file_type] ||= {}
620
+ invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
621
+ end
622
+ # Handle references that are objects with id and type in a hash
623
+ elsif ref_id.is_a?(Hash) && ref_id.key?("id") && ref_id.key?("type")
624
+ id = ref_id["id"]
625
+ type = ref_id["type"]
626
+ composite_key = "#{type}:#{id}"
627
+
628
+ # Try multiple lookup strategies
629
+ valid = false
630
+
631
+ # 1. Try exact composite key match
632
+ valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
633
+
634
+ # 2. Try just ID match if composite didn't work
635
+ valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
636
+
637
+ # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
638
+ if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
639
+ registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
640
+ registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
641
+ registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
642
+ )
643
+ # Special handling for unit_systems between unitsml and nist types
644
+ valid = true
645
+ end
646
+
647
+ unless valid
648
+ invalid_refs[file_type] ||= {}
649
+ invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
650
+ end
651
+ else
652
+ # Handle plain string references (legacy format)
653
+ valid = registry.key?(ref_type) && registry[ref_type].key?(ref_id)
654
+
655
+ unless valid
656
+ invalid_refs[file_type] ||= {}
657
+ invalid_refs[file_type][ref_path] = { id: ref_id, type: ref_type }
658
+ end
659
+ end
660
+ end
661
+ end
662
+ end