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,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module Unitsdb
7
+ module Commands
8
+ module Qudt
9
+ # Updater for adding QUDT references to UnitsDB entities
10
+ module Updater
11
+ QUDT_AUTHORITY = "qudt"
12
+
13
+ module_function
14
+
15
+ # Update references in UnitsDB entities with QUDT references
16
+ def update_references(entity_type, matches, db_entities, output_file,
17
+ include_potential = false)
18
+ puts "Updating QUDT references for #{entity_type}..."
19
+
20
+ # Get the original YAML file path from the database entities
21
+ original_yaml_file = get_original_yaml_file(db_entities, output_file)
22
+
23
+ # Load the original YAML file as plain data structures
24
+ yaml_content = File.read(original_yaml_file)
25
+ output_data = YAML.safe_load(yaml_content)
26
+
27
+ # Create a map of entity IDs to their QUDT references
28
+ entity_references = {}
29
+
30
+ # Process each match
31
+ matches.each do |match|
32
+ db_entity = match[:db_entity]
33
+ qudt_entity = match[:qudt_entity]
34
+
35
+ # Skip potential matches unless specified
36
+ next if match[:potential] && !include_potential
37
+
38
+ # Skip if entity has been manually verified
39
+ if manually_verified?(db_entity)
40
+ puts "Skipping manually verified entity: #{get_entity_id(db_entity)}"
41
+ next
42
+ end
43
+
44
+ # Get entity ID
45
+ entity_id = get_entity_id(db_entity)
46
+ next unless entity_id
47
+
48
+ # Store reference data as plain hash
49
+ entity_references[entity_id] = {
50
+ "uri" => qudt_entity.uri,
51
+ "type" => "informative",
52
+ "authority" => QUDT_AUTHORITY,
53
+ }
54
+ end
55
+
56
+ # Update the YAML content
57
+ output_data[entity_type].each do |entity_yaml|
58
+ # Find entity by ID
59
+ entity_id = if entity_yaml["identifiers"]
60
+ begin
61
+ entity_yaml["identifiers"].first["id"]
62
+ rescue StandardError
63
+ nil
64
+ end
65
+ end
66
+
67
+ next unless entity_id && entity_references.key?(entity_id)
68
+
69
+ # Initialize references array if it doesn't exist
70
+ entity_yaml["references"] ||= []
71
+
72
+ # Add new references
73
+ if (ext_ref = entity_references[entity_id])
74
+ if entity_yaml["references"].any? do |ref|
75
+ ref["uri"] == ext_ref["uri"] && ref["authority"] == ext_ref["authority"]
76
+ end
77
+ # Skip if reference already exists
78
+ puts "Reference already exists for entity ID: #{entity_id}"
79
+ else
80
+ # Add the reference
81
+ puts "Adding reference for entity ID: #{entity_id}, URI: #{ext_ref['uri']}, Authority: #{ext_ref['authority']}"
82
+ entity_yaml["references"] << ext_ref
83
+ end
84
+ end
85
+ end
86
+
87
+ # Write to YAML file
88
+ write_yaml_file(output_file, output_data)
89
+
90
+ puts "Added #{entity_references.values.size} QUDT references to #{entity_type}"
91
+ end
92
+
93
+ # Helper to write YAML file
94
+ def write_yaml_file(output_file, output_data)
95
+ # Ensure the output directory exists
96
+ output_dir = File.dirname(output_file)
97
+ FileUtils.mkdir_p(output_dir)
98
+
99
+ # Write to YAML file with proper formatting
100
+ yaml_content = output_data.to_yaml
101
+
102
+ # Preserve existing schema header or add default one
103
+ yaml_content = preserve_schema_header(output_file, yaml_content)
104
+
105
+ File.write(output_file, yaml_content)
106
+ end
107
+
108
+ # Preserve existing schema header or add default one
109
+ def preserve_schema_header(original_file, yaml_content)
110
+ schema_header = nil
111
+
112
+ # Extract existing schema header if file exists
113
+ if File.exist?(original_file)
114
+ original_content = File.read(original_file)
115
+ if (match = original_content.match(/^# yaml-language-server: \$schema=.+$/))
116
+ schema_header = match[0]
117
+ end
118
+ end
119
+
120
+ # Remove any existing schema header from new content to avoid duplication
121
+ yaml_content = yaml_content.gsub(
122
+ /^# yaml-language-server: \$schema=.+$\n/, ""
123
+ )
124
+
125
+ # Add preserved or default schema header
126
+ if schema_header
127
+ "#{schema_header}\n#{yaml_content}"
128
+ else
129
+ entity_type = File.basename(original_file, ".yaml")
130
+ "# yaml-language-server: $schema=schemas/#{entity_type}-schema.yaml\n#{yaml_content}"
131
+ end
132
+ end
133
+
134
+ # Get the original YAML file path
135
+ def get_original_yaml_file(_db_entities, output_file)
136
+ # The database path should be available from the update command
137
+ # We need to construct the path to the original YAML file
138
+ entity_type = File.basename(output_file, ".yaml")
139
+
140
+ # Try to find the original file in the database directory
141
+ # Look for it relative to where we expect it to be
142
+ # nil
143
+
144
+ # Try to get database directory from environment or assume it's the fixtures
145
+ database_dir = ENV["UNITSDB_DATABASE_PATH"] ||
146
+ File.join(File.dirname(__FILE__),
147
+ "../../../data")
148
+
149
+ original_yaml_file = File.join(database_dir, "#{entity_type}.yaml")
150
+
151
+ # If that doesn't exist, try to find it relative to the current working directory
152
+ unless File.exist?(original_yaml_file)
153
+ original_yaml_file = File.join("data",
154
+ "#{entity_type}.yaml")
155
+ end
156
+
157
+ # If still not found, create a fallback
158
+ unless File.exist?(original_yaml_file)
159
+ puts "Warning: Could not find original YAML file. Creating empty template."
160
+ original_yaml_file = output_file
161
+ FileUtils.mkdir_p(File.dirname(original_yaml_file))
162
+ File.write(original_yaml_file, { entity_type => [] }.to_yaml)
163
+ end
164
+
165
+ puts "Using original YAML file: #{original_yaml_file}"
166
+ original_yaml_file
167
+ end
168
+
169
+ # Get entity ID (either from identifiers array or directly)
170
+ def get_entity_id(entity)
171
+ if entity.respond_to?(:identifiers) && entity.identifiers && !entity.identifiers.empty?
172
+ entity.identifiers.first.id
173
+ elsif entity.respond_to?(:id)
174
+ entity.id
175
+ end
176
+ end
177
+
178
+ # Check if an entity has been manually verified (has a special flag)
179
+ def manually_verified?(entity)
180
+ return false unless entity.respond_to?(:references) && entity.references
181
+
182
+ entity.references.any? do |ref|
183
+ ref.authority == QUDT_AUTHORITY && ref.respond_to?(:verified) && ref.verified
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ module Qudt
8
+ autoload :Check, "unitsdb/commands/qudt/check"
9
+ autoload :Formatter, "unitsdb/commands/qudt/formatter"
10
+ autoload :TtlParser, "unitsdb/commands/qudt/ttl_parser"
11
+ autoload :Update, "unitsdb/commands/qudt/update"
12
+ autoload :Updater, "unitsdb/commands/qudt/updater"
13
+ end
14
+
15
+ class QudtCommand < Thor
16
+ # Inherit trace option from parent CLI
17
+ class_option :trace, type: :boolean, default: false,
18
+ desc: "Show full backtrace on error"
19
+
20
+ desc "check", "Check QUDT references in UnitsDB"
21
+ option :entity_type, type: :string, aliases: "-e",
22
+ desc: "Entity type to check (units, quantities, dimensions, unit_systems). If not specified, all types are checked"
23
+ option :ttl_dir, type: :string, aliases: "-t",
24
+ desc: "Path to directory containing QUDT TTL files. If not specified, vocabularies will be downloaded from online sources"
25
+ option :output_updated_database, type: :string, aliases: "-o",
26
+ desc: "Directory path to write updated YAML files with added QUDT references"
27
+ option :direction, type: :string, default: "both", aliases: "-r",
28
+ desc: "Direction to check: 'to_qudt' (UnitsDB→QUDT), 'from_qudt' (QUDT→UnitsDB), or 'both'"
29
+ option :include_potential_matches, type: :boolean, default: false, aliases: "-p",
30
+ desc: "Include potential matches when updating references (default: false)"
31
+ option :database, type: :string, required: true, aliases: "-d",
32
+ desc: "Path to UnitsDB database (required)"
33
+ def check
34
+ run_command(Qudt::Check, options)
35
+ end
36
+
37
+ desc "update", "Update UnitsDB with QUDT references"
38
+ option :entity_type, type: :string, aliases: "-e",
39
+ desc: "Entity type to update (units, quantities, dimensions, unit_systems). If not specified, all types are updated"
40
+ option :ttl_dir, type: :string, aliases: "-t",
41
+ desc: "Path to directory containing QUDT TTL files. If not specified, vocabularies will be downloaded from online sources"
42
+ option :output_dir, type: :string, aliases: "-o",
43
+ desc: "Directory path to write updated YAML files (defaults to database path)"
44
+ option :include_potential_matches, type: :boolean, default: false, aliases: "-p",
45
+ desc: "Include potential matches when updating references (default: false)"
46
+ option :database, type: :string, required: true, aliases: "-d",
47
+ desc: "Path to UnitsDB database (required)"
48
+ def update
49
+ run_command(Qudt::Update, options)
50
+ end
51
+
52
+ private
53
+
54
+ def run_command(command_class, options)
55
+ command = command_class.new(options)
56
+ command.run
57
+ rescue Unitsdb::Errors::CLIRuntimeError => e
58
+ handle_cli_error(e)
59
+ rescue StandardError => e
60
+ handle_error(e)
61
+ end
62
+
63
+ def handle_cli_error(error)
64
+ if options[:trace]
65
+ raise error
66
+ else
67
+ warn "Error: #{error.message}"
68
+ exit 1
69
+ end
70
+ end
71
+
72
+ def handle_error(error)
73
+ if options[:trace]
74
+ raise error
75
+ else
76
+ warn "Error: #{error.message}"
77
+ exit 1
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,111 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
3
  require "yaml"
5
4
  require "zip"
6
5
  require "fileutils"
7
6
 
8
7
  module Unitsdb
9
8
  module Commands
10
- class Release < Base
9
+ class Release < ::Unitsdb::Commands::Base
11
10
  def run
12
- # Verify all required files exist
13
- yaml_files = Unitsdb::Utils::DEFAULT_YAML_FILES.map { |f| File.join(@options[:database], f) }
14
- missing_files = yaml_files.reject { |f| File.exist?(f) }
15
-
16
- if missing_files.any?
17
- puts "Error: The following required files are missing:"
18
- missing_files.each { |f| puts " - #{f}" }
19
- exit(1)
20
- end
21
-
22
- # Extract schema version from any file (they should all have the same version)
23
- first_yaml = load_yaml(yaml_files.first)
24
- schema_version = first_yaml["schema_version"]
25
-
26
- unless schema_version
27
- puts "Error: Could not determine schema version from #{yaml_files.first}"
28
- exit(1)
29
- end
30
-
31
- # Verify all files have the same schema version
32
- inconsistent_files = []
33
- yaml_files.each do |file|
34
- yaml = load_yaml(file)
35
- next unless yaml["schema_version"] != schema_version
36
-
37
- inconsistent_files << {
38
- file: file,
39
- version: yaml["schema_version"]
40
- }
41
- end
42
-
43
- if inconsistent_files.any?
44
- puts "Error: Inconsistent schema versions detected:"
45
- puts " Expected version: #{schema_version}"
46
- inconsistent_files.each do |info|
47
- puts " - #{info[:file]}: #{info[:version]}"
48
- end
49
- exit(1)
50
- end
11
+ # Load the database
12
+ db = load_database(@options[:database])
13
+ db.version = @options[:version]
51
14
 
52
15
  # Create output directory if it doesn't exist
53
- output_dir = @options[:output_dir] || "."
54
- FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
55
-
56
- # Generate outputs based on format option
57
- format = @options[:format] || "all"
58
-
59
- case format.downcase
60
- when "yaml", "all"
61
- create_unified_yaml(yaml_files, schema_version, output_dir)
62
- end
63
-
64
- case format.downcase
65
- when "zip", "all"
66
- create_zip_archive(yaml_files, schema_version, output_dir)
16
+ FileUtils.mkdir_p(@options[:output_dir])
17
+
18
+ # Generate release files based on format option
19
+ format = (@options[:format] || "all").downcase
20
+ case format
21
+ when "yaml"
22
+ create_unified_yaml(db)
23
+ when "zip"
24
+ create_zip_archive(db)
25
+ when "all"
26
+ create_unified_yaml(db)
27
+ create_zip_archive(db)
28
+ else
29
+ raise Unitsdb::Errors::InvalidFormatError,
30
+ "Invalid format '#{@options[:format]}': must be 'yaml', 'zip', or 'all'"
67
31
  end
68
32
 
69
- puts "Release files created successfully in #{output_dir}"
33
+ puts "Release files created successfully in #{@options[:output_dir]}"
34
+ rescue Unitsdb::Errors::DatabaseError => e
35
+ raise Unitsdb::Errors::DatabaseLoadError,
36
+ "Failed to create release: #{e.message}"
70
37
  end
71
38
 
72
39
  private
73
40
 
74
- def create_unified_yaml(yaml_files, schema_version, output_dir)
75
- # Create a unified YAML structure
76
- unified_yaml = { "schema_version" => schema_version }
77
-
78
- # Load each YAML file and add its collection to the unified structure
79
- yaml_files.each do |file|
80
- yaml = load_yaml(file)
81
- # Get the collection key (units, scales, etc.) - it's the key that's not schema_version
82
- collection_key = (yaml.keys - ["schema_version"]).first
83
- if collection_key
84
- # Add the collection to the unified structure
85
- unified_yaml[collection_key] = yaml[collection_key]
86
- end
87
- end
88
-
89
- # Write the unified YAML to a file
90
- output_file = File.join(output_dir, "unitsdb-#{schema_version}.yaml")
91
- File.write(output_file, unified_yaml.to_yaml)
92
- puts "Created unified YAML file: #{output_file}"
41
+ def create_unified_yaml(db)
42
+ # Create a unified YAML file with all database components
43
+ output_path = File.join(@options[:output_dir],
44
+ "unitsdb-#{@options[:version]}.yaml")
45
+ File.write(output_path, db.to_yaml)
46
+ puts "Created unified YAML file: #{output_path}"
93
47
  end
94
48
 
95
- def create_zip_archive(yaml_files, schema_version, output_dir)
96
- # Create a ZIP archive containing all YAML files
97
- output_file = File.join(output_dir, "unitsdb-#{schema_version}.zip")
98
-
99
- Zip::File.open(output_file, Zip::File::CREATE) do |zipfile|
100
- yaml_files.each do |file|
101
- # Get the filename without the path
102
- filename = File.basename(file)
103
- # Add the file to the ZIP archive
104
- zipfile.add(filename, file)
49
+ def create_zip_archive(db)
50
+ # Create a ZIP archive with individual YAML files
51
+ output_path = File.join(@options[:output_dir],
52
+ "unitsdb-#{@options[:version]}.zip")
53
+
54
+ Zip::File.open(output_path, Zip::File::CREATE) do |zipfile|
55
+ {
56
+ dimensions: Unitsdb::Dimensions,
57
+ unit_systems: Unitsdb::UnitSystems,
58
+ units: Unitsdb::Units,
59
+ prefixes: Unitsdb::Prefixes,
60
+ quantities: Unitsdb::Quantities,
61
+ }.each_pair do |access_method, collection_klass|
62
+ db.send(access_method).tap do |data|
63
+ collection = collection_klass.new(access_method => data)
64
+ collection.version = @options[:version]
65
+ zipfile.get_output_stream("#{access_method}.yaml") do |f|
66
+ f.write(collection.to_yaml)
67
+ end
68
+ end
105
69
  end
106
70
  end
107
71
 
108
- puts "Created ZIP archive: #{output_file}"
72
+ puts "Created ZIP archive: #{output_path}"
109
73
  end
110
74
  end
111
75
  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
@@ -33,8 +31,8 @@ module Unitsdb
33
31
  puts entity.send("to_#{format.downcase}")
34
32
  return
35
33
  rescue NoMethodError
36
- puts "Error: Unable to convert entity to #{format} format"
37
- exit(1)
34
+ raise Unitsdb::Errors::InvalidFormatError,
35
+ "Unable to convert entity to #{format.upcase} format: output format not supported for this entity type"
38
36
  end
39
37
  end
40
38
 
@@ -64,11 +62,10 @@ module Unitsdb
64
62
  print_entity_with_ids(entity)
65
63
  end
66
64
  rescue Unitsdb::Errors::DatabaseError => e
67
- puts "Error: #{e.message}"
68
- exit(1)
65
+ raise Unitsdb::Errors::DatabaseLoadError,
66
+ "Failed to load database: #{e.message}"
69
67
  rescue StandardError => e
70
- puts "Error searching database: #{e.message}"
71
- exit(1)
68
+ raise Unitsdb::Errors::CLIRuntimeError, "Search failed: #{e.message}"
72
69
  end
73
70
  end
74
71
 
@@ -93,7 +90,7 @@ module Unitsdb
93
90
  else
94
91
  puts " IDs:"
95
92
  identifiers.each do |id|
96
- puts " - #{id.id} (Type: #{id.type || "N/A"})"
93
+ puts " - #{id.id} (Type: #{id.type || 'N/A'})"
97
94
  end
98
95
  end
99
96
 
@@ -122,7 +119,7 @@ module Unitsdb
122
119
  if entity.identifiers&.any?
123
120
  puts " - Identifiers:"
124
121
  entity.identifiers.each do |id|
125
- puts " - #{id.id} (Type: #{id.type || "N/A"})"
122
+ puts " - #{id.id} (Type: #{id.type || 'N/A'})"
126
123
  end
127
124
  else
128
125
  puts " - Identifiers: None"
@@ -132,7 +129,11 @@ module Unitsdb
132
129
  case entity
133
130
  when Unitsdb::Unit
134
131
  puts " - Symbols:" if entity.respond_to?(:symbols) && entity.symbols&.any?
135
- entity.symbols.each { |s| puts " - #{s}" } if entity.respond_to?(:symbols) && entity.symbols&.any?
132
+ if entity.respond_to?(:symbols) && entity.symbols&.any?
133
+ entity.symbols.each do |s|
134
+ puts " - #{s}"
135
+ end
136
+ end
136
137
 
137
138
  puts " - Definition: #{entity.definition}" if entity.respond_to?(:definition) && entity.definition
138
139
 
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ module Ucum
8
+ class Check < Base
9
+ # Constants
10
+ ENTITY_TYPES = %w[units prefixes].freeze
11
+
12
+ def run
13
+ # Get options
14
+ entity_type = @options[:entity_type]&.downcase
15
+ direction = @options[:direction]&.downcase || "both"
16
+ output_dir = @options[:output_updated_database]
17
+ include_potential = @options[:include_potential_matches] || false
18
+ database_path = @options[:database]
19
+ ucum_file = @options[:ucum_file]
20
+
21
+ # Validate parameters
22
+ validate_parameters(direction, ucum_file)
23
+
24
+ # Use the path as-is without expansion
25
+ puts "Using database directory: #{database_path}"
26
+
27
+ @db = Unitsdb::Database.from_db(database_path)
28
+
29
+ puts "Using UCUM file: #{ucum_file}"
30
+ puts "Include potential matches: #{include_potential ? 'Yes' : 'No'}"
31
+
32
+ # Parse UCUM XML file
33
+ ucum_data = XmlParser.parse_ucum_file(ucum_file)
34
+
35
+ # Process entity types
36
+ process_entities(entity_type, ucum_data, direction, output_dir,
37
+ include_potential)
38
+ end
39
+
40
+ private
41
+
42
+ # Process all entity types or a specific one
43
+ def process_entities(entity_type, ucum_data, direction, output_dir,
44
+ include_potential)
45
+ if entity_type && ENTITY_TYPES.include?(entity_type)
46
+ process_entity_type(entity_type, ucum_data, direction, output_dir,
47
+ include_potential)
48
+ else
49
+ ENTITY_TYPES.each do |type|
50
+ process_entity_type(type, ucum_data, direction, output_dir,
51
+ include_potential)
52
+ end
53
+ end
54
+ end
55
+
56
+ # Process a specific entity type
57
+ def process_entity_type(entity_type, ucum_data, direction, output_dir,
58
+ include_potential = false)
59
+ puts "\n========== Processing #{entity_type.upcase} References ==========\n"
60
+
61
+ db_entities = @db.send(entity_type)
62
+ ucum_entities = XmlParser.get_entities_from_ucum(entity_type,
63
+ ucum_data)
64
+
65
+ puts "Found #{ucum_entities.size} #{entity_type} in UCUM"
66
+ puts "Found #{db_entities.size} #{entity_type} in database"
67
+
68
+ if %w[from_ucum
69
+ both].include?(direction)
70
+ check_from_ucum(entity_type, ucum_entities, db_entities, output_dir,
71
+ include_potential)
72
+ end
73
+
74
+ return unless %w[to_ucum both].include?(direction)
75
+
76
+ check_to_ucum(entity_type, ucum_entities, db_entities, output_dir,
77
+ include_potential)
78
+ end
79
+
80
+ # Validation helpers
81
+ def validate_parameters(direction, ucum_file)
82
+ unless %w[to_ucum from_ucum both].include?(direction)
83
+ raise Unitsdb::Errors::InvalidParameterError,
84
+ "Invalid direction '#{direction}': must be 'to_ucum', 'from_ucum', or 'both'"
85
+ end
86
+
87
+ return if File.exist?(ucum_file)
88
+
89
+ raise Unitsdb::Errors::FileNotFoundError,
90
+ "UCUM file not found: #{ucum_file}"
91
+ end
92
+
93
+ # Direction handler: UCUM → UnitsDB
94
+ def check_from_ucum(entity_type, ucum_entities, db_entities,
95
+ output_dir, include_potential = false)
96
+ Formatter.print_direction_header("UCUM → UnitsDB")
97
+
98
+ matches, missing_matches, unmatched_ucum = Matcher.match_ucum_to_db(
99
+ entity_type, ucum_entities, db_entities
100
+ )
101
+
102
+ # Print results
103
+ Formatter.display_ucum_results(entity_type, matches, missing_matches,
104
+ unmatched_ucum)
105
+
106
+ # Update references if needed
107
+ return unless output_dir && !missing_matches.empty?
108
+
109
+ output_file = File.join(output_dir, "#{entity_type}.yaml")
110
+ Updater.update_references(entity_type, missing_matches, db_entities,
111
+ output_file, include_potential)
112
+ puts "\nUpdated references written to #{output_file}"
113
+ end
114
+
115
+ # Direction handler: UnitsDB → UCUM
116
+ def check_to_ucum(entity_type, ucum_entities, db_entities, output_dir,
117
+ include_potential = false)
118
+ Formatter.print_direction_header("UnitsDB → UCUM")
119
+
120
+ matches, missing_refs, unmatched_db = Matcher.match_db_to_ucum(
121
+ entity_type, ucum_entities, db_entities
122
+ )
123
+
124
+ # Print results
125
+ Formatter.display_db_results(entity_type, matches, missing_refs,
126
+ unmatched_db)
127
+
128
+ # Update references if needed
129
+ return unless output_dir && !missing_refs.empty?
130
+
131
+ output_file = File.join(output_dir, "#{entity_type}.yaml")
132
+ Updater.update_references(entity_type, missing_refs, db_entities,
133
+ output_file, include_potential)
134
+ puts "\nUpdated references written to #{output_file}"
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end