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
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../base"
4
- require_relative "../../database"
5
- require_relative "xml_parser"
6
- require_relative "matcher"
7
- require_relative "updater"
8
3
  require "fileutils"
9
4
 
10
5
  module Unitsdb
@@ -37,17 +32,19 @@ module Unitsdb
37
32
  return 1
38
33
  end
39
34
  puts "Using UCUM file: #{ucum_file}"
40
- puts "Include potential matches: #{include_potential ? "Yes" : "No"}"
35
+ puts "Include potential matches: #{include_potential ? 'Yes' : 'No'}"
41
36
 
42
37
  # Parse UCUM XML file
43
38
  ucum_data = XmlParser.parse_ucum_file(ucum_file)
44
39
 
45
40
  # Process entity types
46
41
  if entity_type && ENTITY_TYPES.include?(entity_type)
47
- process_entity_type(entity_type, ucum_data, output_dir, include_potential)
42
+ process_entity_type(entity_type, ucum_data, output_dir,
43
+ include_potential)
48
44
  else
49
45
  ENTITY_TYPES.each do |type|
50
- process_entity_type(type, ucum_data, output_dir, include_potential)
46
+ process_entity_type(type, ucum_data, output_dir,
47
+ include_potential)
51
48
  end
52
49
  end
53
50
 
@@ -56,7 +53,8 @@ module Unitsdb
56
53
 
57
54
  private
58
55
 
59
- def process_entity_type(entity_type, ucum_data, output_dir, include_potential)
56
+ def process_entity_type(entity_type, ucum_data, output_dir,
57
+ include_potential)
60
58
  puts "\n========== Processing #{entity_type.upcase} References =========="
61
59
 
62
60
  # Get entities
@@ -64,19 +62,22 @@ module Unitsdb
64
62
  yaml_path = File.join(@options[:database], "#{entity_type}.yaml")
65
63
  entity_collection = klass.from_yaml(File.read(yaml_path))
66
64
 
67
- ucum_entities = XmlParser.get_entities_from_ucum(entity_type, ucum_data)
65
+ ucum_entities = XmlParser.get_entities_from_ucum(entity_type,
66
+ ucum_data)
68
67
 
69
68
  return if ucum_entities.nil? || ucum_entities.empty?
70
69
 
71
70
  # Match entities
72
- _, missing_refs, = Matcher.match_db_to_ucum(entity_type, ucum_entities, entity_collection)
71
+ _, missing_refs, = Matcher.match_db_to_ucum(entity_type,
72
+ ucum_entities, entity_collection)
73
73
 
74
74
  # Create output directory if it doesn't exist
75
- FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
75
+ FileUtils.mkdir_p(output_dir)
76
76
 
77
77
  # Update references in UnitsDB entities
78
78
  output_file = File.join(output_dir, "#{entity_type}.yaml")
79
- Updater.update_references(entity_type, missing_refs, entity_collection, output_file, include_potential)
79
+ Updater.update_references(entity_type, missing_refs,
80
+ entity_collection, output_file, include_potential)
80
81
  end
81
82
  end
82
83
  end
@@ -13,7 +13,8 @@ module Unitsdb
13
13
  module_function
14
14
 
15
15
  # Update references in UnitsDB entities with UCUM references
16
- def update_references(entity_type, matches, db_entities, output_file, include_potential = false)
16
+ def update_references(entity_type, matches, db_entities, output_file,
17
+ include_potential = false)
17
18
  puts "Updating UCUM references for #{entity_type}..."
18
19
 
19
20
  # Create a map of entity IDs to their UCUM references
@@ -35,7 +36,7 @@ module Unitsdb
35
36
  entity_references[entity_id] = ExternalReference.new(
36
37
  uri: ucum_entity.identifier,
37
38
  type: "informative",
38
- authority: UCUM_AUTHORITY
39
+ authority: UCUM_AUTHORITY,
39
40
  )
40
41
  end
41
42
 
@@ -57,7 +58,9 @@ module Unitsdb
57
58
 
58
59
  # Add new references
59
60
  if (ext_ref = entity_references[entity_id])
60
- if entity.references.detect { |ref| ref.uri == ext_ref.uri && ref.authority == ext_ref.authority }
61
+ if entity.references.detect do |ref|
62
+ ref.uri == ext_ref.uri && ref.authority == ext_ref.authority
63
+ end
61
64
  # Skip if reference already exists
62
65
  puts "Reference already exists for entity ID: #{entity_id}"
63
66
  else
@@ -78,10 +81,41 @@ module Unitsdb
78
81
  def write_yaml_file(output_file, output_data)
79
82
  # Ensure the output directory exists
80
83
  output_dir = File.dirname(output_file)
81
- FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
84
+ FileUtils.mkdir_p(output_dir)
82
85
 
83
- # Write to YAML file
84
- File.write(output_file, output_data.to_yaml)
86
+ # Write to YAML file with proper formatting
87
+ yaml_content = output_data.to_yaml
88
+
89
+ # Preserve existing schema header or add default one
90
+ yaml_content = preserve_schema_header(output_file, yaml_content)
91
+
92
+ File.write(output_file, yaml_content)
93
+ end
94
+
95
+ # Preserve existing schema header or add default one
96
+ def preserve_schema_header(original_file, yaml_content)
97
+ schema_header = nil
98
+
99
+ # Extract existing schema header if file exists
100
+ if File.exist?(original_file)
101
+ original_content = File.read(original_file)
102
+ if (match = original_content.match(/^# yaml-language-server: \$schema=.+$/))
103
+ schema_header = match[0]
104
+ end
105
+ end
106
+
107
+ # Remove any existing schema header from new content to avoid duplication
108
+ yaml_content = yaml_content.gsub(
109
+ /^# yaml-language-server: \$schema=.+$\n/, ""
110
+ )
111
+
112
+ # Add preserved or default schema header
113
+ if schema_header
114
+ "#{schema_header}\n#{yaml_content}"
115
+ else
116
+ entity_type = File.basename(original_file, ".yaml")
117
+ "# yaml-language-server: $schema=schemas/#{entity_type}-schema.yaml\n#{yaml_content}"
118
+ end
85
119
  end
86
120
 
87
121
  # Get entity ID (either from identifiers array or directly)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../ucum"
4
-
5
3
  module Unitsdb
6
4
  module Commands
7
5
  module Ucum
@@ -4,7 +4,20 @@ require "thor"
4
4
 
5
5
  module Unitsdb
6
6
  module Commands
7
+ module Ucum
8
+ autoload :Check, "unitsdb/commands/ucum/check"
9
+ autoload :Update, "unitsdb/commands/ucum/update"
10
+ autoload :Formatter, "unitsdb/commands/ucum/formatter"
11
+ autoload :Matcher, "unitsdb/commands/ucum/matcher"
12
+ autoload :Updater, "unitsdb/commands/ucum/updater"
13
+ autoload :XmlParser, "unitsdb/commands/ucum/xml_parser"
14
+ end
15
+
7
16
  class UcumCommand < Thor
17
+ # Inherit trace option from parent CLI
18
+ class_option :trace, type: :boolean, default: false,
19
+ desc: "Show full backtrace on error"
20
+
8
21
  desc "check", "Check UCUM references in UnitsDB"
9
22
  option :entity_type, type: :string, aliases: "-e",
10
23
  desc: "Entity type to check (units, prefixes). If not specified, all types are checked"
@@ -19,8 +32,7 @@ module Unitsdb
19
32
  option :database, type: :string, required: true, aliases: "-d",
20
33
  desc: "Path to UnitsDB database (required)"
21
34
  def check
22
- require_relative "ucum/check"
23
- Ucum::Check.new(options).run
35
+ run_command(Ucum::Check, options)
24
36
  end
25
37
 
26
38
  desc "update", "Update UnitsDB with UCUM references"
@@ -35,8 +47,36 @@ module Unitsdb
35
47
  option :database, type: :string, required: true, aliases: "-d",
36
48
  desc: "Path to UnitsDB database (required)"
37
49
  def update
38
- require_relative "ucum/update"
39
- Ucum::Update.new(options).run
50
+ run_command(Ucum::Update, options)
51
+ end
52
+
53
+ private
54
+
55
+ def run_command(command_class, options)
56
+ command = command_class.new(options)
57
+ command.run
58
+ rescue Unitsdb::Errors::CLIRuntimeError => e
59
+ handle_cli_error(e)
60
+ rescue StandardError => e
61
+ handle_error(e)
62
+ end
63
+
64
+ def handle_cli_error(error)
65
+ if options[:trace]
66
+ raise error
67
+ else
68
+ warn "Error: #{error.message}"
69
+ exit 1
70
+ end
71
+ end
72
+
73
+ def handle_error(error)
74
+ if options[:trace]
75
+ raise error
76
+ else
77
+ warn "Error: #{error.message}"
78
+ exit 1
79
+ end
40
80
  end
41
81
  end
42
82
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../base"
4
-
5
3
  module Unitsdb
6
4
  module Commands
7
5
  module Validate
@@ -12,8 +10,8 @@ module Unitsdb
12
10
 
13
11
  display_results(all_dups)
14
12
  rescue Unitsdb::Errors::DatabaseError => e
15
- puts "Error: #{e.message}"
16
- exit(1)
13
+ raise Unitsdb::Errors::ValidationError,
14
+ "Failed to validate identifiers: #{e.message}"
17
15
  end
18
16
 
19
17
  private
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ module Commands
5
+ module Validate
6
+ class QudtReferences < Unitsdb::Commands::Base
7
+ def run
8
+ # Load the database
9
+ db = load_database(@options[:database])
10
+
11
+ # Check for duplicate QUDT references
12
+ duplicates = check_qudt_references(db)
13
+
14
+ # Display results
15
+ display_duplicate_results(duplicates)
16
+ rescue Unitsdb::Errors::DatabaseError => e
17
+ raise Unitsdb::Errors::ValidationError,
18
+ "Failed to validate QUDT references: #{e.message}"
19
+ end
20
+
21
+ private
22
+
23
+ def check_qudt_references(db)
24
+ duplicates = {}
25
+
26
+ # Check units
27
+ check_entity_qudt_references(db.units, "units", duplicates)
28
+
29
+ # Check quantities
30
+ check_entity_qudt_references(db.quantities, "quantities", duplicates)
31
+
32
+ # Check dimensions
33
+ check_entity_qudt_references(db.dimensions, "dimensions", duplicates)
34
+
35
+ # Check unit_systems
36
+ check_entity_qudt_references(db.unit_systems, "unit_systems",
37
+ duplicates)
38
+
39
+ duplicates
40
+ end
41
+
42
+ def check_entity_qudt_references(entities, entity_type, duplicates)
43
+ # Track QUDT references by URI
44
+ qudt_refs = {}
45
+
46
+ entities.each_with_index do |entity, index|
47
+ # Skip if no references
48
+ next unless entity.respond_to?(:references) && entity.references
49
+
50
+ # Check each reference
51
+ entity.references.each do |ref|
52
+ # Only interested in qudt references
53
+ next unless ref.authority == "qudt"
54
+
55
+ # Get entity info for display
56
+ entity_id = if entity.respond_to?(:identifiers) && entity.identifiers&.first.respond_to?(:id)
57
+ entity.identifiers.first.id
58
+ else
59
+ entity.short
60
+ end
61
+
62
+ # Track this reference
63
+ qudt_refs[ref.uri] ||= []
64
+ qudt_refs[ref.uri] << {
65
+ entity_id: entity_id,
66
+ entity_name: entity.respond_to?(:names) ? entity.names.first : entity.short,
67
+ index: index,
68
+ }
69
+ end
70
+ end
71
+
72
+ # Find duplicates (URIs with more than one entity)
73
+ qudt_refs.each do |uri, entities|
74
+ next unless entities.size > 1
75
+
76
+ # Record this duplicate
77
+ duplicates[entity_type] ||= {}
78
+ duplicates[entity_type][uri] = entities
79
+ end
80
+ end
81
+
82
+ def display_duplicate_results(duplicates)
83
+ if duplicates.empty?
84
+ puts "No duplicate QUDT references found! Each QUDT reference URI is used by at most one entity of each type."
85
+ return
86
+ end
87
+
88
+ puts "Found duplicate QUDT references:"
89
+
90
+ duplicates.each do |entity_type, uri_duplicates|
91
+ puts "\n #{entity_type.capitalize}:"
92
+
93
+ uri_duplicates.each do |uri, entities|
94
+ puts " QUDT URI: #{uri}"
95
+ puts " Used by #{entities.size} entities:"
96
+
97
+ entities.each do |entity|
98
+ puts " - #{entity[:entity_id]} (#{entity[:entity_name]}) at index #{entity[:index]}"
99
+ end
100
+ puts ""
101
+ end
102
+ end
103
+
104
+ puts "\nEach QUDT reference should be used by at most one entity of each type."
105
+ puts "Please fix the duplicates by either removing the reference from all but one entity,"
106
+ puts "or by updating the references to use different URIs appropriate for each entity."
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../base"
4
-
5
3
  module Unitsdb
6
4
  module Commands
7
5
  module Validate
@@ -19,8 +17,8 @@ module Unitsdb
19
17
  # Display results
20
18
  display_reference_results(invalid_refs, registry)
21
19
  rescue Unitsdb::Errors::DatabaseError => e
22
- puts "Error: #{e.message}"
23
- exit(1)
20
+ raise Unitsdb::Errors::ValidationError,
21
+ "Failed to validate references: #{e.message}"
24
22
  end
25
23
 
26
24
  private
@@ -99,7 +97,8 @@ module Unitsdb
99
97
  # Also track unit systems by short name
100
98
  if unit_system.respond_to?(:short) && unit_system.short
101
99
  registry["unit_systems_short"] ||= {}
102
- registry["unit_systems_short"][unit_system.short] = "index:#{index}"
100
+ registry["unit_systems_short"][unit_system.short] =
101
+ "index:#{index}"
103
102
  end
104
103
  end
105
104
 
@@ -143,7 +142,8 @@ module Unitsdb
143
142
  ref_type = "dimensions"
144
143
  ref_path = "dimensions:index:#{index}:dimension_reference"
145
144
 
146
- validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "dimensions")
145
+ validate_reference(ref_id, ref_type, ref_path, registry,
146
+ invalid_refs, "dimensions")
147
147
  end
148
148
  end
149
149
 
@@ -155,7 +155,8 @@ module Unitsdb
155
155
  ref_type = "unit_systems"
156
156
  ref_path = "units:index:#{index}:unit_system_reference[#{idx}]"
157
157
 
158
- validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
158
+ validate_reference(ref_id, ref_type, ref_path, registry,
159
+ invalid_refs, "units")
159
160
  end
160
161
  end
161
162
  end
@@ -168,7 +169,8 @@ module Unitsdb
168
169
  ref_type = "quantities"
169
170
  ref_path = "units:index:#{index}:quantity_references[#{idx}]"
170
171
 
171
- validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
172
+ validate_reference(ref_id, ref_type, ref_path, registry,
173
+ invalid_refs, "units")
172
174
  end
173
175
  end
174
176
  end
@@ -185,7 +187,8 @@ module Unitsdb
185
187
  ref_type = "units"
186
188
  ref_path = "units:index:#{index}:root_units.#{idx}.unit_reference"
187
189
 
188
- validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
190
+ validate_reference(ref_id, ref_type, ref_path, registry,
191
+ invalid_refs, "units")
189
192
 
190
193
  # Check prefix reference if present
191
194
  next unless root_unit.respond_to?(:prefix_reference) && root_unit.prefix_reference
@@ -194,12 +197,14 @@ module Unitsdb
194
197
  ref_type = "prefixes"
195
198
  ref_path = "units:index:#{index}:root_units.#{idx}.prefix_reference"
196
199
 
197
- validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
200
+ validate_reference(ref_id, ref_type, ref_path, registry,
201
+ invalid_refs, "units")
198
202
  end
199
203
  end
200
204
  end
201
205
 
202
- def validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, file_type)
206
+ def validate_reference(ref_id, ref_type, ref_path, registry,
207
+ invalid_refs, file_type)
203
208
  # Handle references that are objects with id and type (could be a hash or an object)
204
209
  if ref_id.respond_to?(:id) && ref_id.respond_to?(:type)
205
210
  id = ref_id.id
@@ -218,8 +223,12 @@ module Unitsdb
218
223
  # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
219
224
  if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
220
225
  registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
221
- registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
222
- registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
226
+ registry[ref_type].keys.any? do |k|
227
+ k.end_with?(":SI_#{id.sub('si-', '')}")
228
+ end ||
229
+ registry[ref_type].keys.any? do |k|
230
+ k.end_with?(":non-SI_#{id.sub('nonsi-', '')}")
231
+ end
223
232
  )
224
233
  # Special handling for unit_systems between unitsml and nist types
225
234
  valid = true
@@ -229,7 +238,8 @@ module Unitsdb
229
238
  puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
230
239
  else
231
240
  invalid_refs[file_type] ||= {}
232
- invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
241
+ invalid_refs[file_type][ref_path] =
242
+ { id: id, type: type, ref_type: ref_type }
233
243
  end
234
244
  # Handle references that are objects with id and type in a hash
235
245
  elsif ref_id.is_a?(Hash) && ref_id.key?("id") && ref_id.key?("type")
@@ -249,8 +259,12 @@ module Unitsdb
249
259
  # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
250
260
  if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
251
261
  registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
252
- registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
253
- registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
262
+ registry[ref_type].keys.any? do |k|
263
+ k.end_with?(":SI_#{id.sub('si-', '')}")
264
+ end ||
265
+ registry[ref_type].keys.any? do |k|
266
+ k.end_with?(":non-SI_#{id.sub('nonsi-', '')}")
267
+ end
254
268
  )
255
269
  # Special handling for unit_systems between unitsml and nist types
256
270
  valid = true
@@ -260,7 +274,8 @@ module Unitsdb
260
274
  puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
261
275
  else
262
276
  invalid_refs[file_type] ||= {}
263
- invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
277
+ invalid_refs[file_type][ref_path] =
278
+ { id: id, type: type, ref_type: ref_type }
264
279
  end
265
280
  else
266
281
  # Handle plain string references (legacy format)
@@ -292,7 +307,8 @@ module Unitsdb
292
307
 
293
308
  puts " #{type}:"
294
309
  ids.each do |id, location|
295
- puts " #{id}: {type: #{type.sub("s", "")}, source: #{location}}"
310
+ puts " #{id}: {type: #{type.sub('s',
311
+ '')}, source: #{location}}"
296
312
  end
297
313
  end
298
314
  end
@@ -304,7 +320,8 @@ module Unitsdb
304
320
  # Suggest corrections
305
321
  next unless registry.key?(ref[:ref_type])
306
322
 
307
- similar_ids = Unitsdb::Utils.find_similar_ids(ref[:id], registry[ref[:ref_type]].keys)
323
+ similar_ids = Unitsdb::Utils.find_similar_ids(ref[:id],
324
+ registry[ref[:ref_type]].keys)
308
325
  if similar_ids.any?
309
326
  puts " Did you mean one of these?"
310
327
  similar_ids.each { |id| puts " - #{id}" }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../base"
4
-
5
3
  module Unitsdb
6
4
  module Commands
7
5
  module Validate
@@ -16,8 +14,8 @@ module Unitsdb
16
14
  # Display results
17
15
  display_duplicate_results(duplicates)
18
16
  rescue Unitsdb::Errors::DatabaseError => e
19
- puts "Error: #{e.message}"
20
- exit(1)
17
+ raise Unitsdb::Errors::ValidationError,
18
+ "Failed to validate SI references: #{e.message}"
21
19
  end
22
20
 
23
21
  private
@@ -62,7 +60,7 @@ module Unitsdb
62
60
  si_refs[ref.uri] << {
63
61
  entity_id: entity_id,
64
62
  entity_name: entity.respond_to?(:names) ? entity.names.first : entity.short,
65
- index: index
63
+ index: index,
66
64
  }
67
65
  end
68
66
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ module Commands
5
+ module Validate
6
+ class UcumReferences < Unitsdb::Commands::Base
7
+ def run
8
+ # Load the database
9
+ db = load_database(@options[:database])
10
+
11
+ # Check for duplicate UCUM references
12
+ duplicates = check_ucum_references(db)
13
+
14
+ # Display results
15
+ display_duplicate_results(duplicates)
16
+ rescue Unitsdb::Errors::DatabaseError => e
17
+ raise Unitsdb::Errors::ValidationError,
18
+ "Failed to validate UCUM references: #{e.message}"
19
+ end
20
+
21
+ private
22
+
23
+ def check_ucum_references(db)
24
+ duplicates = {}
25
+
26
+ # Check units
27
+ check_entity_ucum_references(db.units, "units", duplicates)
28
+
29
+ # Check prefixes
30
+ check_entity_ucum_references(db.prefixes, "prefixes", duplicates)
31
+
32
+ duplicates
33
+ end
34
+
35
+ def check_entity_ucum_references(entities, entity_type, duplicates)
36
+ # Track UCUM references by code
37
+ ucum_refs = {}
38
+
39
+ entities.each_with_index do |entity, index|
40
+ # Skip if no external references
41
+ next unless entity.respond_to?(:external_references) && entity.external_references
42
+
43
+ # Check each external reference
44
+ entity.external_references.each do |ref|
45
+ # Only interested in ucum references
46
+ next unless ref.authority == "ucum"
47
+
48
+ # Get entity info for display
49
+ entity_id = entity.respond_to?(:id) ? entity.id : entity.short
50
+ entity_name = if entity.respond_to?(:names) && entity.names&.first
51
+ entity.names.first.respond_to?(:name) ? entity.names.first.name : entity.names.first
52
+ else
53
+ entity.short
54
+ end
55
+
56
+ # Track this reference
57
+ ucum_refs[ref.code] ||= []
58
+ ucum_refs[ref.code] << {
59
+ entity_id: entity_id,
60
+ entity_name: entity_name,
61
+ index: index,
62
+ }
63
+ end
64
+ end
65
+
66
+ # Find duplicates (codes with more than one entity)
67
+ ucum_refs.each do |code, entities|
68
+ next unless entities.size > 1
69
+
70
+ # Record this duplicate
71
+ duplicates[entity_type] ||= {}
72
+ duplicates[entity_type][code] = entities
73
+ end
74
+ end
75
+
76
+ def display_duplicate_results(duplicates)
77
+ if duplicates.empty?
78
+ puts "No duplicate UCUM references found! Each UCUM reference code is used by at most one entity of each type."
79
+ return
80
+ end
81
+
82
+ puts "Found duplicate UCUM references:"
83
+
84
+ duplicates.each do |entity_type, code_duplicates|
85
+ puts "\n #{entity_type.capitalize}:"
86
+
87
+ code_duplicates.each do |code, entities|
88
+ puts " UCUM Code: #{code}"
89
+ puts " Used by #{entities.size} entities:"
90
+
91
+ entities.each do |entity|
92
+ puts " - #{entity[:entity_id]} (#{entity[:entity_name]}) at index #{entity[:index]}"
93
+ end
94
+ puts ""
95
+ end
96
+ end
97
+
98
+ puts "\nEach UCUM reference should be used by at most one entity of each type."
99
+ puts "Please fix the duplicates by either removing the reference from all but one entity,"
100
+ puts "or by updating the references to use different codes appropriate for each entity."
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end