unitsdb 2.1.0 → 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 +247 -53
  7. data/CLAUDE.md +55 -0
  8. data/Gemfile +5 -1
  9. data/README.adoc +385 -12
  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 +47 -13
  24. data/lib/unitsdb/commands/_modify.rb +41 -5
  25. data/lib/unitsdb/commands/base.rb +10 -32
  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 +50 -86
  41. data/lib/unitsdb/commands/search.rb +12 -11
  42. data/lib/unitsdb/commands/ucum/check.rb +139 -0
  43. data/lib/unitsdb/commands/ucum/formatter.rb +142 -0
  44. data/lib/unitsdb/commands/ucum/matcher.rb +315 -0
  45. data/lib/unitsdb/commands/ucum/update.rb +85 -0
  46. data/lib/unitsdb/commands/ucum/updater.rb +132 -0
  47. data/lib/unitsdb/commands/ucum/xml_parser.rb +32 -0
  48. data/lib/unitsdb/commands/ucum.rb +83 -0
  49. data/lib/unitsdb/commands/validate/identifiers.rb +3 -3
  50. data/lib/unitsdb/commands/validate/qudt_references.rb +111 -0
  51. data/lib/unitsdb/commands/validate/references.rb +37 -18
  52. data/lib/unitsdb/commands/validate/si_references.rb +3 -11
  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 +91 -52
  57. data/lib/unitsdb/dimension.rb +1 -4
  58. data/lib/unitsdb/dimension_details.rb +0 -1
  59. data/lib/unitsdb/dimensions.rb +1 -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 +1 -1
  64. data/lib/unitsdb/quantities.rb +1 -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 +1 -2
  71. data/lib/unitsdb/si_derived_base.rb +0 -2
  72. data/lib/unitsdb/ucum.rb +202 -0
  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 +1 -2
  78. data/lib/unitsdb/units.rb +1 -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 -5
  83. metadata +60 -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,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Unitsdb
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
+
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
+
21
+ desc "check", "Check UCUM references in UnitsDB"
22
+ option :entity_type, type: :string, aliases: "-e",
23
+ desc: "Entity type to check (units, prefixes). If not specified, all types are checked"
24
+ option :ucum_file, type: :string, required: true, aliases: "-u",
25
+ desc: "Path to the UCUM essence XML file"
26
+ option :output_updated_database, type: :string, aliases: "-o",
27
+ desc: "Directory path to write updated YAML files with added UCUM references"
28
+ option :direction, type: :string, default: "both", aliases: "-r",
29
+ desc: "Direction to check: 'to_ucum' (UnitsDB→UCUM), 'from_ucum' (UCUM→UnitsDB), or 'both'"
30
+ option :include_potential_matches, type: :boolean, default: false, aliases: "-p",
31
+ desc: "Include potential matches when updating references (default: false)"
32
+ option :database, type: :string, required: true, aliases: "-d",
33
+ desc: "Path to UnitsDB database (required)"
34
+ def check
35
+ run_command(Ucum::Check, options)
36
+ end
37
+
38
+ desc "update", "Update UnitsDB with UCUM references"
39
+ option :entity_type, type: :string, aliases: "-e",
40
+ desc: "Entity type to update (units, prefixes). If not specified, all types are updated"
41
+ option :ucum_file, type: :string, required: true, aliases: "-u",
42
+ desc: "Path to the UCUM essence XML file"
43
+ option :output_dir, type: :string, aliases: "-o",
44
+ desc: "Directory path to write updated YAML files (defaults to database path)"
45
+ option :include_potential_matches, type: :boolean, default: false, aliases: "-p",
46
+ desc: "Include potential matches when updating references (default: false)"
47
+ option :database, type: :string, required: true, aliases: "-d",
48
+ desc: "Path to UnitsDB database (required)"
49
+ def update
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
80
+ end
81
+ end
82
+ end
83
+ end
@@ -3,15 +3,15 @@
3
3
  module Unitsdb
4
4
  module Commands
5
5
  module Validate
6
- class Identifiers < Base
6
+ class Identifiers < Unitsdb::Commands::Base
7
7
  def run
8
8
  db = load_database
9
9
  all_dups = db.validate_uniqueness
10
10
 
11
11
  display_results(all_dups)
12
12
  rescue Unitsdb::Errors::DatabaseError => e
13
- puts "Error: #{e.message}"
14
- exit(1)
13
+ raise Unitsdb::Errors::ValidationError,
14
+ "Failed to validate identifiers: #{e.message}"
15
15
  end
16
16
 
17
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
@@ -3,7 +3,7 @@
3
3
  module Unitsdb
4
4
  module Commands
5
5
  module Validate
6
- class References < Base
6
+ class References < Unitsdb::Commands::Base
7
7
  def run
8
8
  # Load the database
9
9
  db = load_database(@options[:database])
@@ -17,8 +17,8 @@ module Unitsdb
17
17
  # Display results
18
18
  display_reference_results(invalid_refs, registry)
19
19
  rescue Unitsdb::Errors::DatabaseError => e
20
- puts "Error: #{e.message}"
21
- exit(1)
20
+ raise Unitsdb::Errors::ValidationError,
21
+ "Failed to validate references: #{e.message}"
22
22
  end
23
23
 
24
24
  private
@@ -97,7 +97,8 @@ module Unitsdb
97
97
  # Also track unit systems by short name
98
98
  if unit_system.respond_to?(:short) && unit_system.short
99
99
  registry["unit_systems_short"] ||= {}
100
- registry["unit_systems_short"][unit_system.short] = "index:#{index}"
100
+ registry["unit_systems_short"][unit_system.short] =
101
+ "index:#{index}"
101
102
  end
102
103
  end
103
104
 
@@ -141,7 +142,8 @@ module Unitsdb
141
142
  ref_type = "dimensions"
142
143
  ref_path = "dimensions:index:#{index}:dimension_reference"
143
144
 
144
- 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")
145
147
  end
146
148
  end
147
149
 
@@ -153,7 +155,8 @@ module Unitsdb
153
155
  ref_type = "unit_systems"
154
156
  ref_path = "units:index:#{index}:unit_system_reference[#{idx}]"
155
157
 
156
- 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")
157
160
  end
158
161
  end
159
162
  end
@@ -166,7 +169,8 @@ module Unitsdb
166
169
  ref_type = "quantities"
167
170
  ref_path = "units:index:#{index}:quantity_references[#{idx}]"
168
171
 
169
- 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")
170
174
  end
171
175
  end
172
176
  end
@@ -183,7 +187,8 @@ module Unitsdb
183
187
  ref_type = "units"
184
188
  ref_path = "units:index:#{index}:root_units.#{idx}.unit_reference"
185
189
 
186
- 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")
187
192
 
188
193
  # Check prefix reference if present
189
194
  next unless root_unit.respond_to?(:prefix_reference) && root_unit.prefix_reference
@@ -192,12 +197,14 @@ module Unitsdb
192
197
  ref_type = "prefixes"
193
198
  ref_path = "units:index:#{index}:root_units.#{idx}.prefix_reference"
194
199
 
195
- 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")
196
202
  end
197
203
  end
198
204
  end
199
205
 
200
- 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)
201
208
  # Handle references that are objects with id and type (could be a hash or an object)
202
209
  if ref_id.respond_to?(:id) && ref_id.respond_to?(:type)
203
210
  id = ref_id.id
@@ -216,8 +223,12 @@ module Unitsdb
216
223
  # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
217
224
  if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
218
225
  registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
219
- registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
220
- 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
221
232
  )
222
233
  # Special handling for unit_systems between unitsml and nist types
223
234
  valid = true
@@ -227,7 +238,8 @@ module Unitsdb
227
238
  puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
228
239
  else
229
240
  invalid_refs[file_type] ||= {}
230
- 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 }
231
243
  end
232
244
  # Handle references that are objects with id and type in a hash
233
245
  elsif ref_id.is_a?(Hash) && ref_id.key?("id") && ref_id.key?("type")
@@ -247,8 +259,12 @@ module Unitsdb
247
259
  # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
248
260
  if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
249
261
  registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
250
- registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
251
- 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
252
268
  )
253
269
  # Special handling for unit_systems between unitsml and nist types
254
270
  valid = true
@@ -258,7 +274,8 @@ module Unitsdb
258
274
  puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
259
275
  else
260
276
  invalid_refs[file_type] ||= {}
261
- 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 }
262
279
  end
263
280
  else
264
281
  # Handle plain string references (legacy format)
@@ -290,7 +307,8 @@ module Unitsdb
290
307
 
291
308
  puts " #{type}:"
292
309
  ids.each do |id, location|
293
- puts " #{id}: {type: #{type.sub("s", "")}, source: #{location}}"
310
+ puts " #{id}: {type: #{type.sub('s',
311
+ '')}, source: #{location}}"
294
312
  end
295
313
  end
296
314
  end
@@ -302,7 +320,8 @@ module Unitsdb
302
320
  # Suggest corrections
303
321
  next unless registry.key?(ref[:ref_type])
304
322
 
305
- 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)
306
325
  if similar_ids.any?
307
326
  puts " Did you mean one of these?"
308
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
@@ -103,12 +101,6 @@ module Unitsdb
103
101
  puts "Please fix the duplicates by either removing the reference from all but one entity,"
104
102
  puts "or by updating the references to use different URIs appropriate for each entity."
105
103
  end
106
-
107
- def load_database(path)
108
- Unitsdb::Database.from_db(path)
109
- rescue StandardError => e
110
- raise Unitsdb::Errors::DatabaseError, "Failed to load database: #{e.message}"
111
- end
112
104
  end
113
105
  end
114
106
  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
@@ -4,16 +4,28 @@ require "thor"
4
4
 
5
5
  module Unitsdb
6
6
  module Commands
7
+ module Validate
8
+ autoload :Identifiers, "unitsdb/commands/validate/identifiers"
9
+ autoload :QudtReferences, "unitsdb/commands/validate/qudt_references"
10
+ autoload :References, "unitsdb/commands/validate/references"
11
+ autoload :SiReferences, "unitsdb/commands/validate/si_references"
12
+ autoload :UcumReferences, "unitsdb/commands/validate/ucum_references"
13
+ end
14
+
7
15
  class ValidateCommand < Thor
16
+ # Inherit trace option from parent CLI
17
+ class_option :trace, type: :boolean, default: false,
18
+ desc: "Show full backtrace on error"
19
+
8
20
  desc "references", "Validate that all references exist"
9
- option :debug_registry, type: :boolean, desc: "Show registry contents for debugging"
21
+ option :debug_registry, type: :boolean,
22
+ desc: "Show registry contents for debugging"
10
23
  option :database, type: :string, required: true, aliases: "-d",
11
24
  desc: "Path to UnitsDB database (required)"
12
- option :print_valid, type: :boolean, default: false, desc: "Print valid references too"
25
+ option :print_valid, type: :boolean, default: false,
26
+ desc: "Print valid references too"
13
27
  def references
14
- require_relative "validate/references"
15
-
16
- Commands::References.new(options).run
28
+ run_command(Commands::Validate::References, options)
17
29
  end
18
30
 
19
31
  desc "identifiers", "Check for uniqueness of identifier fields"
@@ -21,19 +33,63 @@ module Unitsdb
21
33
  desc: "Path to UnitsDB database (required)"
22
34
 
23
35
  def identifiers
24
- require_relative "validate/identifiers"
25
-
26
- Commands::Identifiers.new(options).run
36
+ run_command(Commands::Validate::Identifiers, options)
27
37
  end
28
38
 
29
- desc "si_references", "Validate that each SI digital framework reference is unique per entity type"
39
+ desc "si_references",
40
+ "Validate that each SI digital framework reference is unique per entity type"
30
41
  option :database, type: :string, required: true, aliases: "-d",
31
42
  desc: "Path to UnitsDB database (required)"
32
43
 
33
44
  def si_references
34
- require_relative "validate/si_references"
45
+ run_command(Commands::Validate::SiReferences, options)
46
+ end
47
+
48
+ desc "qudt_references",
49
+ "Validate that each QUDT reference is unique per entity type"
50
+ option :database, type: :string, required: true, aliases: "-d",
51
+ desc: "Path to UnitsDB database (required)"
52
+
53
+ def qudt_references
54
+ run_command(Commands::Validate::QudtReferences, options)
55
+ end
56
+
57
+ desc "ucum_references",
58
+ "Validate that each UCUM reference is unique per entity type"
59
+ option :database, type: :string, required: true, aliases: "-d",
60
+ desc: "Path to UnitsDB database (required)"
61
+
62
+ def ucum_references
63
+ run_command(Commands::Validate::UcumReferences, options)
64
+ end
65
+
66
+ private
67
+
68
+ def run_command(command_class, options)
69
+ command = command_class.new(options)
70
+ command.run
71
+ rescue Unitsdb::Errors::CLIRuntimeError => e
72
+ handle_cli_error(e)
73
+ rescue StandardError => e
74
+ handle_error(e)
75
+ end
76
+
77
+ def handle_cli_error(error)
78
+ if options[:trace]
79
+ raise error
80
+ else
81
+ warn "Error: #{error.message}"
82
+ exit 1
83
+ end
84
+ end
35
85
 
36
- Validate::SiReferences.new(options).run
86
+ def handle_error(error)
87
+ if options[:trace]
88
+ raise error
89
+ else
90
+ warn "Error: #{error.message}"
91
+ exit 1
92
+ end
37
93
  end
38
94
  end
39
95
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ module Commands
5
+ autoload :ModifyCommand, "unitsdb/commands/_modify"
6
+ autoload :Base, "unitsdb/commands/base"
7
+ autoload :CheckSi, "unitsdb/commands/check_si"
8
+ autoload :CheckSiCommand, "unitsdb/commands/check_si"
9
+ autoload :Get, "unitsdb/commands/get"
10
+ autoload :Normalize, "unitsdb/commands/normalize"
11
+ autoload :Qudt, "unitsdb/commands/qudt"
12
+ autoload :QudtCommand, "unitsdb/commands/qudt"
13
+ autoload :Release, "unitsdb/commands/release"
14
+ autoload :Search, "unitsdb/commands/search"
15
+ autoload :Ucum, "unitsdb/commands/ucum"
16
+ autoload :UcumCommand, "unitsdb/commands/ucum"
17
+ autoload :Validate, "unitsdb/commands/validate"
18
+ autoload :ValidateCommand, "unitsdb/commands/validate"
19
+ end
20
+ end