unitsdb 1.0.0 → 2.1.0

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