unitsdb 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +8 -1
  3. data/.gitignore +2 -0
  4. data/.gitmodules +4 -3
  5. data/.rubocop.yml +13 -8
  6. data/.rubocop_todo.yml +217 -100
  7. data/CLAUDE.md +55 -0
  8. data/Gemfile +4 -1
  9. data/README.adoc +283 -16
  10. data/data/dimensions.yaml +1864 -0
  11. data/data/prefixes.yaml +874 -0
  12. data/data/quantities.yaml +3715 -0
  13. data/data/scales.yaml +97 -0
  14. data/data/schemas/dimensions-schema.yaml +153 -0
  15. data/data/schemas/prefixes-schema.yaml +155 -0
  16. data/data/schemas/quantities-schema.yaml +117 -0
  17. data/data/schemas/scales-schema.yaml +106 -0
  18. data/data/schemas/unit_systems-schema.yaml +116 -0
  19. data/data/schemas/units-schema.yaml +215 -0
  20. data/data/unit_systems.yaml +78 -0
  21. data/data/units.yaml +14052 -0
  22. data/exe/unitsdb +7 -1
  23. data/lib/unitsdb/cli.rb +42 -15
  24. data/lib/unitsdb/commands/_modify.rb +40 -4
  25. data/lib/unitsdb/commands/base.rb +6 -2
  26. data/lib/unitsdb/commands/check_si/si_formatter.rb +488 -0
  27. data/lib/unitsdb/commands/check_si/si_matcher.rb +487 -0
  28. data/lib/unitsdb/commands/check_si/si_ttl_parser.rb +103 -0
  29. data/lib/unitsdb/commands/check_si/si_updater.rb +254 -0
  30. data/lib/unitsdb/commands/check_si.rb +54 -35
  31. data/lib/unitsdb/commands/get.rb +11 -10
  32. data/lib/unitsdb/commands/normalize.rb +21 -7
  33. data/lib/unitsdb/commands/qudt/check.rb +150 -0
  34. data/lib/unitsdb/commands/qudt/formatter.rb +194 -0
  35. data/lib/unitsdb/commands/qudt/matcher.rb +746 -0
  36. data/lib/unitsdb/commands/qudt/ttl_parser.rb +403 -0
  37. data/lib/unitsdb/commands/qudt/update.rb +126 -0
  38. data/lib/unitsdb/commands/qudt/updater.rb +189 -0
  39. data/lib/unitsdb/commands/qudt.rb +82 -0
  40. data/lib/unitsdb/commands/release.rb +12 -9
  41. data/lib/unitsdb/commands/search.rb +12 -11
  42. data/lib/unitsdb/commands/ucum/check.rb +42 -29
  43. data/lib/unitsdb/commands/ucum/formatter.rb +2 -1
  44. data/lib/unitsdb/commands/ucum/matcher.rb +23 -9
  45. data/lib/unitsdb/commands/ucum/update.rb +14 -13
  46. data/lib/unitsdb/commands/ucum/updater.rb +40 -6
  47. data/lib/unitsdb/commands/ucum/xml_parser.rb +0 -2
  48. data/lib/unitsdb/commands/ucum.rb +44 -4
  49. data/lib/unitsdb/commands/validate/identifiers.rb +2 -4
  50. data/lib/unitsdb/commands/validate/qudt_references.rb +111 -0
  51. data/lib/unitsdb/commands/validate/references.rb +36 -19
  52. data/lib/unitsdb/commands/validate/si_references.rb +3 -5
  53. data/lib/unitsdb/commands/validate/ucum_references.rb +105 -0
  54. data/lib/unitsdb/commands/validate.rb +67 -11
  55. data/lib/unitsdb/commands.rb +20 -0
  56. data/lib/unitsdb/database.rb +90 -52
  57. data/lib/unitsdb/dimension.rb +1 -4
  58. data/lib/unitsdb/dimension_details.rb +0 -1
  59. data/lib/unitsdb/dimensions.rb +0 -2
  60. data/lib/unitsdb/errors.rb +7 -0
  61. data/lib/unitsdb/prefix.rb +0 -4
  62. data/lib/unitsdb/prefix_reference.rb +0 -2
  63. data/lib/unitsdb/prefixes.rb +0 -1
  64. data/lib/unitsdb/quantities.rb +0 -2
  65. data/lib/unitsdb/quantity.rb +0 -6
  66. data/lib/unitsdb/qudt.rb +100 -0
  67. data/lib/unitsdb/root_unit_reference.rb +0 -3
  68. data/lib/unitsdb/scale.rb +0 -4
  69. data/lib/unitsdb/scale_reference.rb +0 -2
  70. data/lib/unitsdb/scales.rb +0 -2
  71. data/lib/unitsdb/si_derived_base.rb +0 -2
  72. data/lib/unitsdb/ucum.rb +14 -10
  73. data/lib/unitsdb/unit.rb +0 -10
  74. data/lib/unitsdb/unit_reference.rb +0 -2
  75. data/lib/unitsdb/unit_system.rb +1 -3
  76. data/lib/unitsdb/unit_system_reference.rb +0 -2
  77. data/lib/unitsdb/unit_systems.rb +0 -2
  78. data/lib/unitsdb/units.rb +0 -2
  79. data/lib/unitsdb/utils.rb +32 -21
  80. data/lib/unitsdb/version.rb +5 -1
  81. data/lib/unitsdb.rb +62 -14
  82. data/unitsdb.gemspec +6 -3
  83. metadata +52 -13
  84. data/lib/unitsdb/commands/si_formatter.rb +0 -485
  85. data/lib/unitsdb/commands/si_matcher.rb +0 -470
  86. data/lib/unitsdb/commands/si_ttl_parser.rb +0 -100
  87. data/lib/unitsdb/commands/si_updater.rb +0 -212
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module Unitsdb
7
+ module Commands
8
+ module CheckSi
9
+ # Updater for SI references in YAML
10
+ module SiUpdater
11
+ SI_AUTHORITY = "si-digital-framework"
12
+
13
+ module_function
14
+
15
+ # Update references in YAML file (TTL → DB direction)
16
+ def update_references(entity_type, missing_matches, db_entities,
17
+ output_file, include_potential = false)
18
+ # Use the database objects to access the data directly
19
+ original_yaml_file = db_entities.first.send(:yaml_file) if db_entities&.first.respond_to?(
20
+ :yaml_file, true
21
+ )
22
+
23
+ # If we can't get the path from the database object, use the output file path as a fallback
24
+ if original_yaml_file.nil? || !File.exist?(original_yaml_file)
25
+ puts "Warning: Could not determine original YAML file path. Using output file as template."
26
+ original_yaml_file = output_file
27
+
28
+ # Create an empty template if output file doesn't exist
29
+ unless File.exist?(original_yaml_file)
30
+ FileUtils.mkdir_p(File.dirname(original_yaml_file))
31
+ File.write(original_yaml_file, { entity_type => [] }.to_yaml)
32
+ end
33
+ end
34
+
35
+ # Load the original YAML file
36
+ yaml_content = File.read(original_yaml_file)
37
+ output_data = YAML.safe_load(yaml_content)
38
+
39
+ # Group by entity ID to avoid duplicates
40
+ grouped_matches = missing_matches.group_by do |match|
41
+ match[:entity_id]
42
+ end
43
+
44
+ # Process each entity that needs updating
45
+ grouped_matches.each do |entity_id, matches|
46
+ # Filter matches based on include_potential parameter
47
+ filtered_matches = matches.select do |match|
48
+ # Check if it's an exact match or if we're including potential matches
49
+ match_details = match[:match_details]
50
+ if match_details&.dig(:exact) == false || %w[symbol_match
51
+ partial_match].include?(match_details&.dig(:match_desc) || "")
52
+ include_potential
53
+ else
54
+ true
55
+ end
56
+ end
57
+
58
+ # Skip if no matches after filtering
59
+ next if filtered_matches.empty?
60
+
61
+ # Find the entity in the array under the entity_type key
62
+ entity_index = output_data[entity_type].find_index do |e|
63
+ # Find entity with matching identifier
64
+ e["identifiers"]&.any? { |id| id["id"] == entity_id }
65
+ end
66
+
67
+ next unless entity_index
68
+
69
+ # Get the entity
70
+ entity = output_data[entity_type][entity_index]
71
+
72
+ # Initialize references array if it doesn't exist
73
+ entity["references"] ||= []
74
+
75
+ # Add new references
76
+ filtered_matches.each do |match|
77
+ # If this match has multiple SI references, add them all
78
+ if match[:multiple_si]
79
+ match[:multiple_si].each do |si_data|
80
+ # Check if reference already exists
81
+ next if entity["references"].any? do |ref|
82
+ ref["uri"] == si_data[:uri] && ref["authority"] == SI_AUTHORITY
83
+ end
84
+
85
+ # Add new reference
86
+ entity["references"] << {
87
+ "uri" => si_data[:uri],
88
+ "type" => "normative",
89
+ "authority" => SI_AUTHORITY,
90
+ }
91
+ end
92
+ else
93
+ # Check if reference already exists
94
+ next if entity["references"].any? do |ref|
95
+ ref["uri"] == match[:si_uri] && ref["authority"] == SI_AUTHORITY
96
+ end
97
+
98
+ # Add new reference
99
+ entity["references"] << {
100
+ "uri" => match[:si_uri],
101
+ "type" => "normative",
102
+ "authority" => SI_AUTHORITY,
103
+ }
104
+ end
105
+ end
106
+ end
107
+
108
+ write_yaml_file(output_file, output_data)
109
+ end
110
+
111
+ # Update references in YAML file (DB → TTL direction)
112
+ def update_db_references(entity_type, missing_refs, output_file,
113
+ include_potential = false)
114
+ # Try to get the original YAML file from the first entity
115
+ first_entity = missing_refs.first&.dig(:db_entity)
116
+ original_yaml_file = first_entity.send(:yaml_file) if first_entity.respond_to?(
117
+ :yaml_file, true
118
+ )
119
+
120
+ # If we can't get the path from the database object, use the output file path as a fallback
121
+ if original_yaml_file.nil? || !File.exist?(original_yaml_file)
122
+ puts "Warning: Could not determine original YAML file path. Using output file as template."
123
+ original_yaml_file = output_file
124
+
125
+ # Create an empty template if output file doesn't exist
126
+ unless File.exist?(original_yaml_file)
127
+ FileUtils.mkdir_p(File.dirname(original_yaml_file))
128
+ File.write(original_yaml_file, { entity_type => [] }.to_yaml)
129
+ end
130
+ end
131
+
132
+ # Load the original YAML file
133
+ yaml_content = File.read(original_yaml_file)
134
+ output_data = YAML.safe_load(yaml_content)
135
+
136
+ # Group by entity ID to avoid duplicates
137
+ missing_refs_by_id = {}
138
+
139
+ missing_refs.each do |match|
140
+ entity_id = match[:entity_id] || match[:db_entity].short
141
+ ttl_entities = match[:ttl_entities]
142
+ match_types = match[:match_types] || {}
143
+
144
+ # Filter TTL entities based on include_potential parameter
145
+ filtered_ttl_entities = ttl_entities.select do |ttl_entity|
146
+ # Check if it's an exact match or if we're including potential matches
147
+ match_type = match_types[ttl_entity[:uri]] || "Exact match" # Default to exact match
148
+ match_pair_key = "#{entity_id}:#{ttl_entity[:uri]}"
149
+ match_details = Unitsdb::Commands::CheckSi::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
150
+
151
+ if match_details && %w[symbol_match
152
+ partial_match].include?(match_details[:match_desc])
153
+ include_potential
154
+ else
155
+ match_type == "Exact match" || include_potential
156
+ end
157
+ end
158
+
159
+ # Skip if no entities after filtering
160
+ next if filtered_ttl_entities.empty?
161
+
162
+ missing_refs_by_id[entity_id] ||= []
163
+
164
+ # Add filtered matching TTL entities for this DB entity
165
+ filtered_ttl_entities.each do |ttl_entity|
166
+ missing_refs_by_id[entity_id] << {
167
+ uri: ttl_entity[:uri],
168
+ type: "normative",
169
+ authority: SI_AUTHORITY,
170
+ }
171
+ end
172
+ end
173
+
174
+ # Update the YAML content
175
+ output_data[entity_type].each do |entity_yaml|
176
+ # Find entity by ID or short
177
+ entity_id = if entity_yaml["identifiers"]
178
+ begin
179
+ entity_yaml["identifiers"].first["id"]
180
+ rescue StandardError
181
+ nil
182
+ end
183
+ elsif entity_yaml["id"]
184
+ entity_yaml["id"]
185
+ end
186
+
187
+ next unless entity_id && missing_refs_by_id.key?(entity_id)
188
+
189
+ # Add references
190
+ entity_yaml["references"] ||= []
191
+
192
+ missing_refs_by_id[entity_id].each do |ref|
193
+ # Check if this reference already exists
194
+ next if entity_yaml["references"].any? do |existing_ref|
195
+ existing_ref["uri"] == ref[:uri] &&
196
+ existing_ref["authority"] == ref[:authority]
197
+ end
198
+
199
+ # Add the reference
200
+ entity_yaml["references"] << {
201
+ "uri" => ref[:uri],
202
+ "type" => ref[:type],
203
+ "authority" => ref[:authority],
204
+ }
205
+ end
206
+ end
207
+
208
+ write_yaml_file(output_file, output_data)
209
+ end
210
+
211
+ # Helper to write YAML file
212
+ def write_yaml_file(output_file, output_data)
213
+ # Ensure the output directory exists
214
+ output_dir = File.dirname(output_file)
215
+ FileUtils.mkdir_p(output_dir)
216
+
217
+ # Write to YAML file with proper formatting
218
+ yaml_content = output_data.to_yaml
219
+
220
+ # Preserve existing schema header or add default one
221
+ yaml_content = preserve_schema_header(output_file, yaml_content)
222
+
223
+ File.write(output_file, yaml_content)
224
+ end
225
+
226
+ # Preserve existing schema header or add default one
227
+ def preserve_schema_header(original_file, yaml_content)
228
+ schema_header = nil
229
+
230
+ # Extract existing schema header if file exists
231
+ if File.exist?(original_file)
232
+ original_content = File.read(original_file)
233
+ if (match = original_content.match(/^# yaml-language-server: \$schema=.+$/))
234
+ schema_header = match[0]
235
+ end
236
+ end
237
+
238
+ # Remove any existing schema header from new content to avoid duplication
239
+ yaml_content = yaml_content.gsub(
240
+ /^# yaml-language-server: \$schema=.+$\n/, ""
241
+ )
242
+
243
+ # Add preserved or default schema header
244
+ if schema_header
245
+ "#{schema_header}\n#{yaml_content}"
246
+ else
247
+ entity_type = File.basename(original_file, ".yaml")
248
+ "# yaml-language-server: $schema=schemas/#{entity_type}-schema.yaml\n#{yaml_content}"
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
- require_relative "../database"
5
- require_relative "../errors"
6
- require_relative "si_ttl_parser"
7
- require_relative "si_formatter"
8
- require_relative "si_matcher"
9
- require_relative "si_updater"
10
-
11
3
  module Unitsdb
12
4
  module Commands
13
- class CheckSi < Base
5
+ module CheckSi
6
+ autoload :SiTtlParser, "unitsdb/commands/check_si/si_ttl_parser"
7
+ autoload :SiMatcher, "unitsdb/commands/check_si/si_matcher"
8
+ autoload :SiFormatter, "unitsdb/commands/check_si/si_formatter"
9
+ autoload :SiUpdater, "unitsdb/commands/check_si/si_updater"
10
+ end
11
+
12
+ class CheckSiCommand < Base
14
13
  # Constants
15
14
  ENTITY_TYPES = %w[units quantities prefixes].freeze
16
15
 
@@ -32,91 +31,111 @@ module Unitsdb
32
31
  @db = Unitsdb::Database.from_db(database_path)
33
32
 
34
33
  puts "Using TTL directory: #{ttl_dir}"
35
- puts "Include potential matches: #{include_potential ? "Yes" : "No"}"
34
+ puts "Include potential matches: #{include_potential ? 'Yes' : 'No'}"
36
35
 
37
36
  # Parse TTL files
38
- graph = SiTtlParser.parse_ttl_files(ttl_dir)
37
+ graph = ::Unitsdb::Commands::CheckSi::SiTtlParser.parse_ttl_files(ttl_dir)
39
38
 
40
39
  # Process entity types
41
- process_entities(entity_type, graph, direction, output_dir, include_potential)
40
+ process_entities(entity_type, graph, direction, output_dir,
41
+ include_potential)
42
42
  end
43
43
 
44
44
  private
45
45
 
46
46
  # Process all entity types or a specific one
47
- def process_entities(entity_type, graph, direction, output_dir, include_potential)
47
+ def process_entities(entity_type, graph, direction, output_dir,
48
+ include_potential)
48
49
  if entity_type && ENTITY_TYPES.include?(entity_type)
49
- process_entity_type(entity_type, graph, direction, output_dir, include_potential)
50
+ process_entity_type(entity_type, graph, direction, output_dir,
51
+ include_potential)
50
52
  else
51
53
  ENTITY_TYPES.each do |type|
52
- process_entity_type(type, graph, direction, output_dir, include_potential)
54
+ process_entity_type(type, graph, direction, output_dir,
55
+ include_potential)
53
56
  end
54
57
  end
55
58
  end
56
59
 
57
60
  # Process a specific entity type
58
- def process_entity_type(entity_type, graph, direction, output_dir, include_potential = false)
61
+ def process_entity_type(entity_type, graph, direction, output_dir,
62
+ include_potential = false)
59
63
  puts "\n========== Processing #{entity_type.upcase} References ==========\n"
60
64
 
61
65
  db_entities = @db.send(entity_type)
62
- ttl_entities = SiTtlParser.extract_entities_from_ttl(entity_type, graph)
66
+ ttl_entities = ::Unitsdb::Commands::CheckSi::SiTtlParser.extract_entities_from_ttl(
67
+ entity_type, graph
68
+ )
63
69
 
64
70
  puts "Found #{ttl_entities.size} #{entity_type} in SI digital framework"
65
71
  puts "Found #{db_entities.size} #{entity_type} in database"
66
72
 
67
- check_from_si(entity_type, ttl_entities, db_entities, output_dir, include_potential) if %w[from_si
68
- both].include?(direction)
73
+ if %w[from_si
74
+ both].include?(direction)
75
+ check_from_si(entity_type, ttl_entities, db_entities, output_dir,
76
+ include_potential)
77
+ end
69
78
 
70
79
  return unless %w[to_si both].include?(direction)
71
80
 
72
- check_to_si(entity_type, ttl_entities, db_entities, output_dir, include_potential)
81
+ check_to_si(entity_type, ttl_entities, db_entities, output_dir,
82
+ include_potential)
73
83
  end
74
84
 
75
85
  # Validation helpers
76
86
  def validate_parameters(direction, ttl_dir)
77
87
  unless %w[to_si from_si both].include?(direction)
78
- puts "Invalid direction: #{direction}. Must be one of: to_si, from_si, both"
79
- exit(1)
88
+ raise Unitsdb::Errors::InvalidParameterError,
89
+ "Invalid direction '#{direction}': must be 'to_si', 'from_si', or 'both'"
80
90
  end
81
91
 
82
92
  return if Dir.exist?(ttl_dir)
83
93
 
84
- puts "TTL directory not found: #{ttl_dir}"
85
- exit(1)
94
+ raise Unitsdb::Errors::FileNotFoundError,
95
+ "TTL directory not found: #{ttl_dir}"
86
96
  end
87
97
 
88
98
  # Direction handler: TTL → DB
89
- def check_from_si(entity_type, ttl_entities, db_entities, output_dir, include_potential = false)
90
- SiFormatter.print_direction_header("SI UnitsDB")
99
+ def check_from_si(entity_type, ttl_entities, db_entities, output_dir,
100
+ include_potential = false)
101
+ ::Unitsdb::Commands::CheckSi::SiFormatter.print_direction_header("SI → UnitsDB")
91
102
 
92
- matches, missing_matches, unmatched_ttl = SiMatcher.match_ttl_to_db(entity_type, ttl_entities, db_entities)
103
+ matches, missing_matches, unmatched_ttl = ::Unitsdb::Commands::CheckSi::SiMatcher.match_ttl_to_db(
104
+ entity_type, ttl_entities, db_entities
105
+ )
93
106
 
94
107
  # Print results
95
- SiFormatter.display_si_results(entity_type, matches, missing_matches, unmatched_ttl)
108
+ ::Unitsdb::Commands::CheckSi::SiFormatter.display_si_results(entity_type, matches, missing_matches,
109
+ unmatched_ttl)
96
110
 
97
111
  # Update references if needed
98
112
  return unless output_dir && !missing_matches.empty?
99
113
 
100
114
  output_file = File.join(output_dir, "#{entity_type}.yaml")
101
- SiUpdater.update_references(entity_type, missing_matches, db_entities, output_file, include_potential,
102
- database_path)
115
+ ::Unitsdb::Commands::CheckSi::SiUpdater.update_references(entity_type, missing_matches, db_entities, output_file, include_potential,
116
+ database_path)
103
117
  puts "\nUpdated references written to #{output_file}"
104
118
  end
105
119
 
106
120
  # Direction handler: DB → TTL
107
- def check_to_si(entity_type, ttl_entities, db_entities, output_dir, include_potential = false)
108
- SiFormatter.print_direction_header("UnitsDB SI")
121
+ def check_to_si(entity_type, ttl_entities, db_entities, output_dir,
122
+ include_potential = false)
123
+ ::Unitsdb::Commands::CheckSi::SiFormatter.print_direction_header("UnitsDB → SI")
109
124
 
110
- matches, missing_refs, unmatched_db = SiMatcher.match_db_to_ttl(entity_type, ttl_entities, db_entities)
125
+ matches, missing_refs, unmatched_db = ::Unitsdb::Commands::CheckSi::SiMatcher.match_db_to_ttl(
126
+ entity_type, ttl_entities, db_entities
127
+ )
111
128
 
112
129
  # Print results
113
- SiFormatter.display_db_results(entity_type, matches, missing_refs, unmatched_db)
130
+ ::Unitsdb::Commands::CheckSi::SiFormatter.display_db_results(entity_type, matches, missing_refs,
131
+ unmatched_db)
114
132
 
115
133
  # Update references if needed
116
134
  return unless output_dir && !missing_refs.empty?
117
135
 
118
136
  output_file = File.join(output_dir, "#{entity_type}.yaml")
119
- SiUpdater.update_db_references(entity_type, missing_refs, output_file, include_potential, @options[:database])
137
+ ::Unitsdb::Commands::CheckSi::SiUpdater.update_db_references(entity_type, missing_refs, output_file,
138
+ include_potential, @options[:database])
120
139
  puts "\nUpdated references written to #{output_file}"
121
140
  end
122
141
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
3
  require "json"
5
- require_relative "../errors"
6
4
 
7
5
  module Unitsdb
8
6
  module Commands
@@ -29,19 +27,18 @@ module Unitsdb
29
27
  puts entity.send("to_#{format.downcase}")
30
28
  return
31
29
  rescue NoMethodError
32
- puts "Error: Unable to convert entity to #{format} format"
33
- exit(1)
30
+ raise Unitsdb::Errors::InvalidFormatError,
31
+ "Unable to convert entity to #{format.upcase} format: output format not supported for this entity type"
34
32
  end
35
33
  end
36
34
 
37
35
  # Default text output
38
36
  print_entity_details(entity)
39
37
  rescue Unitsdb::Errors::DatabaseError => e
40
- puts "Error: #{e.message}"
41
- exit(1)
38
+ raise Unitsdb::Errors::DatabaseLoadError,
39
+ "Failed to load database: #{e.message}"
42
40
  rescue StandardError => e
43
- puts "Error searching database: #{e.message}"
44
- exit(1)
41
+ raise Unitsdb::Errors::CLIRuntimeError, "Search failed: #{e.message}"
45
42
  end
46
43
  end
47
44
 
@@ -65,7 +62,7 @@ module Unitsdb
65
62
  if entity.identifiers&.any?
66
63
  puts " - Identifiers:"
67
64
  entity.identifiers.each do |id|
68
- puts " - #{id.id} (Type: #{id.type || "N/A"})"
65
+ puts " - #{id.id} (Type: #{id.type || 'N/A'})"
69
66
  end
70
67
  else
71
68
  puts " - Identifiers: None"
@@ -75,7 +72,11 @@ module Unitsdb
75
72
  case entity
76
73
  when Unitsdb::Unit
77
74
  puts " - Symbols:" if entity.respond_to?(:symbols) && entity.symbols&.any?
78
- entity.symbols.each { |s| puts " - #{s}" } if entity.respond_to?(:symbols) && entity.symbols&.any?
75
+ if entity.respond_to?(:symbols) && entity.symbols&.any?
76
+ entity.symbols.each do |s|
77
+ puts " - #{s}"
78
+ end
79
+ end
79
80
 
80
81
  puts " - Definition: #{entity.definition}" if entity.respond_to?(:definition) && entity.definition
81
82
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
3
  require "yaml"
5
4
 
6
5
  module Unitsdb
@@ -8,8 +7,8 @@ module Unitsdb
8
7
  class Normalize < Base
9
8
  def run(input = nil, output = nil)
10
9
  unless @options[:all] || (input && output)
11
- puts "Error: INPUT and OUTPUT are required when not using --all"
12
- exit(1)
10
+ raise Unitsdb::Errors::InvalidParameterError,
11
+ "INPUT and OUTPUT files are required unless --all flag is specified"
13
12
  end
14
13
 
15
14
  if @options[:all]
@@ -32,8 +31,18 @@ module Unitsdb
32
31
  private
33
32
 
34
33
  def normalize_file(input, output)
34
+ # Read the original file content to check for schema comment
35
+ file_content = File.read(input)
36
+
37
+ # Check if the first line is a yaml-language-server schema comment
38
+ schema_comment = nil
39
+ lines = file_content.lines
40
+ if lines.first&.start_with?("# yaml-language-server:")
41
+ schema_comment = lines.first.chomp
42
+ end
43
+
35
44
  # Load the original YAML to work with
36
- yaml = YAML.safe_load(File.read(input))
45
+ yaml = YAML.safe_load(file_content)
37
46
 
38
47
  # For schema 2.0.0, we need to handle the schema_version and the main collection key
39
48
  if yaml.key?("schema_version") && yaml["schema_version"] == "2.0.0"
@@ -47,7 +56,8 @@ module Unitsdb
47
56
  when "nist", "unitsml"
48
57
  # Sort by ID (nist or unitsml)
49
58
  id_type = @options[:sort]
50
- yaml[collection_key] = sort_by_id_type(yaml[collection_key], id_type)
59
+ yaml[collection_key] =
60
+ sort_by_id_type(yaml[collection_key], id_type)
51
61
  else # default to "short"
52
62
  # Use the existing sort_yaml_keys method for default sorting
53
63
  yaml[collection_key] = Unitsdb::Utils.sort_yaml_keys(yaml[collection_key])
@@ -58,8 +68,12 @@ module Unitsdb
58
68
  yaml = Unitsdb::Utils.sort_yaml_keys(yaml)
59
69
  end
60
70
 
61
- # Write the normalized output
62
- File.write(output, yaml.to_yaml)
71
+ # Write the normalized output, preserving schema comment if present
72
+ output_content = yaml.to_yaml
73
+ if schema_comment
74
+ output_content = "#{schema_comment}\n#{output_content}"
75
+ end
76
+ File.write(output, output_content)
63
77
  end
64
78
 
65
79
  # Sort collection items by a specific ID type (nist or unitsml)
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ module Qudt
8
+ class Check < Base
9
+ # Constants
10
+ ENTITY_TYPES = %w[units quantities dimensions unit_systems
11
+ prefixes].freeze
12
+
13
+ def run
14
+ # Get options
15
+ entity_type = @options[:entity_type]&.downcase
16
+ direction = @options[:direction]&.downcase || "both"
17
+ output_dir = @options[:output_updated_database]
18
+ include_potential = @options[:include_potential_matches] || false
19
+ database_path = @options[:database]
20
+ ttl_dir = @options[:ttl_dir]
21
+ source_type = ttl_dir ? :file : :url
22
+
23
+ # Validate parameters
24
+ validate_parameters(direction, ttl_dir, source_type)
25
+
26
+ # Use the path as-is without expansion
27
+ puts "Using database directory: #{database_path}"
28
+
29
+ @db = Unitsdb::Database.from_db(database_path)
30
+
31
+ if source_type == :file
32
+ puts "Using QUDT TTL directory: #{ttl_dir}"
33
+ else
34
+ puts "Downloading QUDT vocabularies from online sources"
35
+ end
36
+ puts "Include potential matches: #{include_potential ? 'Yes' : 'No'}"
37
+
38
+ # Parse QUDT vocabularies
39
+ qudt_data = TtlParser.parse_qudt_vocabularies(
40
+ source_type: source_type, ttl_dir: ttl_dir,
41
+ )
42
+
43
+ # Process entity types
44
+ process_entities(entity_type, qudt_data, direction, output_dir,
45
+ include_potential)
46
+ end
47
+
48
+ private
49
+
50
+ # Process all entity types or a specific one
51
+ def process_entities(entity_type, qudt_data, direction, output_dir,
52
+ include_potential)
53
+ if entity_type && ENTITY_TYPES.include?(entity_type)
54
+ process_entity_type(entity_type, qudt_data, direction, output_dir,
55
+ include_potential)
56
+ else
57
+ ENTITY_TYPES.each do |type|
58
+ process_entity_type(type, qudt_data, direction, output_dir,
59
+ include_potential)
60
+ end
61
+ end
62
+ end
63
+
64
+ # Process a specific entity type
65
+ def process_entity_type(entity_type, qudt_data, direction, output_dir,
66
+ include_potential = false)
67
+ puts "\n========== Processing #{entity_type.upcase} References ==========\n"
68
+
69
+ db_entities = @db.send(entity_type)
70
+ qudt_entities = TtlParser.get_entities_from_qudt(entity_type,
71
+ qudt_data)
72
+
73
+ puts "Found #{qudt_entities.size} #{entity_type} in QUDT"
74
+ puts "Found #{db_entities.size} #{entity_type} in database"
75
+
76
+ if %w[from_qudt
77
+ both].include?(direction)
78
+ check_from_qudt(entity_type, qudt_entities, db_entities, output_dir,
79
+ include_potential)
80
+ end
81
+
82
+ return unless %w[to_qudt both].include?(direction)
83
+
84
+ check_to_qudt(entity_type, qudt_entities, db_entities, output_dir,
85
+ include_potential)
86
+ end
87
+
88
+ # Validation helpers
89
+ def validate_parameters(direction, ttl_dir, source_type)
90
+ unless %w[to_qudt from_qudt both].include?(direction)
91
+ raise Unitsdb::Errors::InvalidParameterError,
92
+ "Invalid direction '#{direction}': must be 'to_qudt', 'from_qudt', or 'both'"
93
+ end
94
+
95
+ return unless source_type == :file && ttl_dir && !Dir.exist?(ttl_dir)
96
+
97
+ raise Unitsdb::Errors::FileNotFoundError,
98
+ "TTL directory not found: #{ttl_dir}"
99
+ end
100
+
101
+ # Direction handler: QUDT → UnitsDB
102
+ def check_from_qudt(entity_type, qudt_entities, db_entities,
103
+ output_dir, include_potential = false)
104
+ Formatter.print_direction_header("QUDT → UnitsDB")
105
+
106
+ matches, missing_matches, unmatched_qudt = Matcher.match_qudt_to_db(
107
+ entity_type, qudt_entities, db_entities
108
+ )
109
+
110
+ # Print results
111
+ Formatter.display_qudt_results(entity_type, matches, missing_matches,
112
+ unmatched_qudt)
113
+
114
+ # Display detailed missing QUDT entities analysis
115
+ Formatter.display_missing_qudt_entities(entity_type, unmatched_qudt)
116
+
117
+ # Update references if needed
118
+ return unless output_dir && !missing_matches.empty?
119
+
120
+ output_file = File.join(output_dir, "#{entity_type}.yaml")
121
+ Updater.update_references(entity_type, missing_matches, db_entities,
122
+ output_file, include_potential)
123
+ puts "\nUpdated references written to #{output_file}"
124
+ end
125
+
126
+ # Direction handler: UnitsDB → QUDT
127
+ def check_to_qudt(entity_type, qudt_entities, db_entities, output_dir,
128
+ include_potential = false)
129
+ Formatter.print_direction_header("UnitsDB → QUDT")
130
+
131
+ matches, missing_refs, unmatched_db = Matcher.match_db_to_qudt(
132
+ entity_type, qudt_entities, db_entities
133
+ )
134
+
135
+ # Print results
136
+ Formatter.display_db_results(entity_type, matches, missing_refs,
137
+ unmatched_db)
138
+
139
+ # Update references if needed
140
+ return unless output_dir && !missing_refs.empty?
141
+
142
+ output_file = File.join(output_dir, "#{entity_type}.yaml")
143
+ Updater.update_references(entity_type, missing_refs, db_entities,
144
+ output_file, include_potential)
145
+ puts "\nUpdated references written to #{output_file}"
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end