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,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ module Commands
5
+ module Ucum
6
+ # Formats output for UCUM matching results
7
+ module Formatter
8
+ module_function
9
+
10
+ # Print a direction header (UnitsDB → UCUM or UCUM → UnitsDB)
11
+ def print_direction_header(direction)
12
+ puts "\n=== #{direction} ===\n"
13
+ end
14
+
15
+ # Display results for UCUM → UnitsDB matching
16
+ def display_ucum_results(entity_type, matches, missing_matches,
17
+ unmatched_ucum)
18
+ puts "\nResults for #{entity_type.capitalize} (UCUM → UnitsDB):"
19
+ puts " Matched: #{matches.size}"
20
+ puts " Missing matches (could be added): #{missing_matches.size}"
21
+ puts " Unmatched UCUM entities: #{unmatched_ucum.size}"
22
+
23
+ unless unmatched_ucum.empty?
24
+ puts "\nUnmatched UCUM #{entity_type}:"
25
+ unmatched_ucum.each do |entity|
26
+ case entity
27
+ when Unitsdb::UcumPrefix
28
+ puts " - #{entity.name} (#{entity.code_sensitive})"
29
+ when Unitsdb::UcumBaseUnit
30
+ puts " - #{entity.name} (#{entity.code_sensitive}, dimension: #{entity.dimension})"
31
+ when Unitsdb::UcumUnit
32
+ name = entity.name.is_a?(Array) ? entity.name.first : entity.name
33
+ puts " - #{name} (#{entity.code_sensitive}, class: #{entity.klass})"
34
+ else
35
+ puts " - Unknown entity type"
36
+ end
37
+ end
38
+ end
39
+
40
+ return if missing_matches.empty?
41
+
42
+ puts "\nPotential additions (UCUM #{entity_type} that could be added to UnitsDB):"
43
+ missing_matches.each do |match|
44
+ ucum_entity = match[:ucum_entity]
45
+ db_entity = match[:db_entity]
46
+
47
+ # Get entity IDs and names
48
+ db_id = get_db_entity_id(db_entity)
49
+ db_name = get_db_entity_name(db_entity)
50
+ ucum_name = get_ucum_entity_name(ucum_entity)
51
+ ucum_code = ucum_entity.respond_to?(:code_sensitive) ? ucum_entity.code_sensitive : "unknown"
52
+
53
+ case ucum_entity
54
+ when Unitsdb::UcumPrefix
55
+ puts " - UnitsDB prefix '#{db_name}' (#{db_id}) → UCUM prefix '#{ucum_name}' (#{ucum_code})"
56
+ when Unitsdb::UcumBaseUnit
57
+ puts " - UnitsDB unit '#{db_name}' (#{db_id}) → UCUM base unit '#{ucum_name}' (#{ucum_code})"
58
+ when Unitsdb::UcumUnit
59
+ puts " - UnitsDB unit '#{db_name}' (#{db_id}) → UCUM unit '#{ucum_name}' (#{ucum_code})"
60
+ end
61
+ end
62
+ end
63
+
64
+ # Display results for UnitsDB → UCUM matching
65
+ def display_db_results(entity_type, matches, missing_refs, unmatched_db)
66
+ puts "\nResults for #{entity_type.capitalize} (UnitsDB → UCUM):"
67
+ puts " Matched: #{matches.size}"
68
+ puts " Missing references (could be added): #{missing_refs.size}"
69
+ puts " Unmatched UnitsDB entities: #{unmatched_db.size}"
70
+
71
+ unless unmatched_db.empty?
72
+ puts "\nUnmatched UnitsDB #{entity_type}:"
73
+ unmatched_db.each do |entity|
74
+ id = get_db_entity_id(entity)
75
+ name = get_db_entity_name(entity)
76
+ puts " - #{name} (#{id})"
77
+ end
78
+ end
79
+
80
+ return if missing_refs.empty?
81
+
82
+ puts "\nPotential references (UCUM references that could be added to UnitsDB):"
83
+ missing_refs.each do |match|
84
+ ucum_entity = match[:ucum_entity]
85
+ db_entity = match[:db_entity]
86
+
87
+ # Get entity IDs and names
88
+ db_id = get_db_entity_id(db_entity)
89
+ db_name = get_db_entity_name(db_entity)
90
+ ucum_name = get_ucum_entity_name(ucum_entity)
91
+ ucum_code = ucum_entity.respond_to?(:code_sensitive) ? ucum_entity.code_sensitive : "unknown"
92
+
93
+ case ucum_entity
94
+ when Unitsdb::UcumPrefix
95
+ puts " - UnitsDB prefix '#{db_name}' (#{db_id}) → UCUM prefix '#{ucum_name}' (#{ucum_code})"
96
+ when Unitsdb::UcumBaseUnit
97
+ puts " - UnitsDB unit '#{db_name}' (#{db_id}) → UCUM base unit '#{ucum_name}' (#{ucum_code})"
98
+ when Unitsdb::UcumUnit
99
+ puts " - UnitsDB unit '#{db_name}' (#{db_id}) → UCUM unit '#{ucum_name}' (#{ucum_code})"
100
+ end
101
+ end
102
+ end
103
+
104
+ # Helper to get db entity id
105
+ def get_db_entity_id(entity)
106
+ if entity.respond_to?(:identifiers) && entity.identifiers && !entity.identifiers.empty?
107
+ entity.identifiers.first.id
108
+ elsif entity.respond_to?(:id)
109
+ entity.id
110
+ else
111
+ "unknown-id"
112
+ end
113
+ end
114
+
115
+ # Helper to get db entity name
116
+ def get_db_entity_name(entity)
117
+ if entity.respond_to?(:names) && entity.names && !entity.names.empty?
118
+ entity.names.first.value
119
+ elsif entity.respond_to?(:short) && entity.short
120
+ entity.short
121
+ elsif entity.respond_to?(:name)
122
+ entity.name
123
+ else
124
+ "unknown-name"
125
+ end
126
+ end
127
+
128
+ # Helper to get ucum entity name
129
+ def get_ucum_entity_name(entity)
130
+ case entity
131
+ when Unitsdb::UcumPrefix, Unitsdb::UcumBaseUnit
132
+ entity.name
133
+ when Unitsdb::UcumUnit
134
+ entity.name.is_a?(Array) ? entity.name.first : entity.name
135
+ else
136
+ "unknown-name"
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ module Commands
5
+ module Ucum
6
+ # Matcher for UCUM and UnitsDB entities
7
+ module Matcher
8
+ module_function
9
+
10
+ # Match UCUM entities to UnitsDB entities (UCUM → UnitsDB)
11
+ def match_ucum_to_db(entity_type, ucum_entities, db_entities)
12
+ puts "Matching UCUM #{entity_type} to UnitsDB #{entity_type}..."
13
+
14
+ # Initialize result arrays
15
+ matches = []
16
+ missing_matches = []
17
+ unmatched_ucum = []
18
+
19
+ # Process each UCUM entity
20
+ ucum_entities.each do |ucum_entity|
21
+ match_data = find_db_match_for_ucum(ucum_entity, db_entities,
22
+ entity_type)
23
+
24
+ if match_data[:match]
25
+ matches << { ucum_entity: ucum_entity,
26
+ db_entity: match_data[:match] }
27
+ elsif match_data[:potential_match]
28
+ missing_matches << { ucum_entity: ucum_entity,
29
+ db_entity: match_data[:potential_match] }
30
+ else
31
+ unmatched_ucum << ucum_entity
32
+ end
33
+ end
34
+
35
+ [matches, missing_matches, unmatched_ucum]
36
+ end
37
+
38
+ # Match UnitsDB entities to UCUM entities (UnitsDB → UCUM)
39
+ def match_db_to_ucum(entity_type, ucum_entities, db_entities)
40
+ puts "Matching UnitsDB #{entity_type} to UCUM #{entity_type}..."
41
+
42
+ # Initialize result arrays
43
+ matches = []
44
+ missing_refs = []
45
+ unmatched_db = []
46
+
47
+ # Process each UnitsDB entity
48
+ db_entities.send(entity_type).each do |db_entity|
49
+ # Skip entities that already have UCUM references
50
+ if has_ucum_reference?(db_entity)
51
+ matches << { db_entity: db_entity,
52
+ ucum_entity: find_referenced_ucum_entity(db_entity,
53
+ ucum_entities) }
54
+ next
55
+ end
56
+
57
+ match_data = find_ucum_match_for_db(db_entity, ucum_entities,
58
+ entity_type)
59
+
60
+ if match_data[:match]
61
+ missing_refs << { db_entity: db_entity,
62
+ ucum_entity: match_data[:match] }
63
+ else
64
+ unmatched_db << db_entity
65
+ end
66
+ end
67
+
68
+ [matches, missing_refs, unmatched_db]
69
+ end
70
+
71
+ # Check if a UnitsDB entity already has a UCUM reference
72
+ def has_ucum_reference?(entity)
73
+ return false unless entity.respond_to?(:references) && entity.references
74
+
75
+ entity.references.any? { |ref| ref.authority == "ucum" }
76
+ end
77
+
78
+ # Find the referenced UCUM entity based on the reference URI
79
+ def find_referenced_ucum_entity(db_entity, ucum_entities)
80
+ return nil unless db_entity.respond_to?(:references) && db_entity.references
81
+
82
+ ucum_ref = db_entity.references.find { |ref| ref.authority == "ucum" }
83
+ return nil unless ucum_ref
84
+
85
+ ref_uri = ucum_ref.uri
86
+ ucum_entities.find { |ucum_entity| ucum_entity.identifier == ref_uri }
87
+ end
88
+
89
+ # Get the ID of a UnitsDB entity
90
+ def get_entity_id(entity)
91
+ entity.respond_to?(:id) ? entity.id : nil
92
+ end
93
+
94
+ # Find a matching UnitsDB entity for a UCUM entity
95
+ def find_db_match_for_ucum(ucum_entity, db_entities, entity_type)
96
+ result = { match: nil, potential_match: nil }
97
+
98
+ # Different matching logic based on entity type
99
+ case entity_type
100
+ when "prefixes"
101
+ result = match_prefix_ucum_to_db(ucum_entity, db_entities)
102
+ when "units"
103
+ result = match_unit_ucum_to_db(ucum_entity, db_entities)
104
+ end
105
+
106
+ result
107
+ end
108
+
109
+ # Find a matching UCUM entity for a UnitsDB entity
110
+ def find_ucum_match_for_db(db_entity, ucum_entities, entity_type)
111
+ result = { match: nil }
112
+
113
+ # Different matching logic based on entity type
114
+ case entity_type
115
+ when "prefixes"
116
+ result = match_prefix_db_to_ucum(db_entity, ucum_entities)
117
+ when "units"
118
+ result = match_unit_db_to_ucum(db_entity, ucum_entities)
119
+ end
120
+
121
+ result
122
+ end
123
+
124
+ # Match UCUM prefix to UnitsDB prefix
125
+ def match_prefix_ucum_to_db(ucum_prefix, db_prefixes)
126
+ result = { match: nil, potential_match: nil }
127
+
128
+ # Try exact name match first
129
+ name_match = db_prefixes.find do |db_prefix|
130
+ db_prefix.names&.any? do |name_obj|
131
+ name_obj.value.downcase == ucum_prefix.name.downcase
132
+ end
133
+ end
134
+
135
+ if name_match
136
+ result[:match] = name_match
137
+ return result
138
+ end
139
+
140
+ # Try symbol match
141
+ symbol_match = db_prefixes.find do |db_prefix|
142
+ db_prefix.symbols&.any? do |symbol|
143
+ symbol.ascii == ucum_prefix.print_symbol ||
144
+ symbol.unicode == ucum_prefix.print_symbol
145
+ end
146
+ end
147
+
148
+ if symbol_match
149
+ result[:match] = symbol_match
150
+ return result
151
+ end
152
+
153
+ # Try value match if available (using base^power)
154
+ if ucum_prefix.value&.value
155
+ value_match = db_prefixes.find do |db_prefix|
156
+ if db_prefix.base && db_prefix.power
157
+ calculated_value = db_prefix.base**db_prefix.power
158
+ calculated_value.to_s == ucum_prefix.value.value
159
+ else
160
+ false
161
+ end
162
+ end
163
+
164
+ result[:potential_match] = value_match if value_match
165
+ end
166
+
167
+ result
168
+ end
169
+
170
+ # Match UnitsDB prefix to UCUM prefix
171
+ def match_prefix_db_to_ucum(db_prefix, ucum_prefixes)
172
+ result = { match: nil }
173
+
174
+ # Try exact name match first
175
+ if db_prefix.names && !db_prefix.names.empty?
176
+ db_prefix_names = db_prefix.names.map do |name_obj|
177
+ name_obj.value.downcase
178
+ end
179
+
180
+ name_match = ucum_prefixes.find do |ucum_prefix|
181
+ db_prefix_names.include?(ucum_prefix.name.downcase)
182
+ end
183
+
184
+ if name_match
185
+ result[:match] = name_match
186
+ return result
187
+ end
188
+ end
189
+
190
+ # Try symbol match
191
+ if db_prefix.symbols && !db_prefix.symbols.empty?
192
+ symbol_match = ucum_prefixes.find do |ucum_prefix|
193
+ db_prefix.symbols.any? do |symbol|
194
+ ucum_prefix.print_symbol == symbol.ascii ||
195
+ ucum_prefix.print_symbol == symbol.unicode
196
+ end
197
+ end
198
+
199
+ result[:match] = symbol_match if symbol_match
200
+ end
201
+
202
+ result
203
+ end
204
+
205
+ # Match UCUM unit to UnitsDB unit
206
+ def match_unit_ucum_to_db(ucum_unit, db_units)
207
+ result = { match: nil, potential_match: nil }
208
+
209
+ # Get UCUM unit name(s)
210
+ ucum_names = case ucum_unit
211
+ when Unitsdb::UcumBaseUnit
212
+ [ucum_unit.name]
213
+ when Unitsdb::UcumUnit
214
+ ucum_unit.name.is_a?(Array) ? ucum_unit.name : [ucum_unit.name]
215
+ else
216
+ []
217
+ end
218
+
219
+ # Try name match
220
+ ucum_names.each do |ucum_name|
221
+ name_match = db_units.find do |db_unit|
222
+ db_unit.names&.any? do |name_obj|
223
+ name_obj.value.downcase == ucum_name.downcase
224
+ end
225
+ end
226
+
227
+ if name_match
228
+ result[:match] = name_match
229
+ return result
230
+ end
231
+ end
232
+
233
+ # Try symbol match
234
+ symbol_match = db_units.find do |db_unit|
235
+ db_unit.symbols&.any? do |symbol|
236
+ symbol.ascii == ucum_unit.print_symbol ||
237
+ symbol.unicode == ucum_unit.print_symbol
238
+ end
239
+ end
240
+
241
+ if symbol_match
242
+ result[:match] = symbol_match
243
+ return result
244
+ end
245
+
246
+ # Try property/dimension match for potential matches
247
+ property = case ucum_unit
248
+ when Unitsdb::UcumBaseUnit
249
+ ucum_unit.property
250
+ when Unitsdb::UcumUnit
251
+ ucum_unit.property
252
+ end
253
+
254
+ if property
255
+ property_matches = db_units.select do |db_unit|
256
+ db_unit.quantity_references&.any? do |qref|
257
+ qref.id&.downcase&.include?(property.downcase)
258
+ end
259
+ end
260
+
261
+ if property_matches.any?
262
+ result[:potential_match] =
263
+ property_matches.first
264
+ end
265
+ end
266
+
267
+ result
268
+ end
269
+
270
+ # Match UnitsDB unit to UCUM unit
271
+ def match_unit_db_to_ucum(db_unit, ucum_units)
272
+ result = { match: nil }
273
+
274
+ # Try name match first
275
+ if db_unit.names && !db_unit.names.empty?
276
+ db_unit_names = db_unit.names.map do |name_obj|
277
+ name_obj.value.downcase
278
+ end
279
+
280
+ name_match = ucum_units.find do |ucum_unit|
281
+ case ucum_unit
282
+ when Unitsdb::UcumBaseUnit
283
+ db_unit_names.include?(ucum_unit.name.downcase)
284
+ when Unitsdb::UcumUnit
285
+ ucum_names = ucum_unit.name.is_a?(Array) ? ucum_unit.name : [ucum_unit.name]
286
+ ucum_names.any? { |name| db_unit_names.include?(name.downcase) }
287
+ else
288
+ false
289
+ end
290
+ end
291
+
292
+ if name_match
293
+ result[:match] = name_match
294
+ return result
295
+ end
296
+ end
297
+
298
+ # Try symbol match
299
+ if db_unit.symbols && !db_unit.symbols.empty?
300
+ symbol_match = ucum_units.find do |ucum_unit|
301
+ db_unit.symbols.any? do |symbol|
302
+ ucum_unit.print_symbol == symbol.ascii ||
303
+ ucum_unit.print_symbol == symbol.unicode
304
+ end
305
+ end
306
+
307
+ result[:match] = symbol_match if symbol_match
308
+ end
309
+
310
+ result
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ module Ucum
8
+ # Command to update UnitsDB with UCUM references
9
+ class Update < Base
10
+ # Constants
11
+ ENTITY_TYPES = %w[units prefixes].freeze
12
+
13
+ def run
14
+ # Get options
15
+ entity_type = @options[:entity_type]&.downcase
16
+ database_path = @options[:database]
17
+ ucum_file = @options[:ucum_file]
18
+ output_dir = @options[:output_dir] || database_path
19
+ include_potential = @options[:include_potential_matches] || false
20
+
21
+ # Validate database path
22
+ unless File.exist?(database_path) && Dir.exist?(database_path)
23
+ puts "Database directory path: #{database_path}"
24
+ puts "ERROR: Database directory not found: #{database_path}"
25
+ return 1
26
+ end
27
+ puts "Using database directory: #{database_path}"
28
+
29
+ # Validate UCUM file
30
+ unless File.exist?(ucum_file)
31
+ puts "ERROR: UCUM file not found: #{ucum_file}"
32
+ return 1
33
+ end
34
+ puts "Using UCUM file: #{ucum_file}"
35
+ puts "Include potential matches: #{include_potential ? 'Yes' : 'No'}"
36
+
37
+ # Parse UCUM XML file
38
+ ucum_data = XmlParser.parse_ucum_file(ucum_file)
39
+
40
+ # Process entity types
41
+ if entity_type && ENTITY_TYPES.include?(entity_type)
42
+ process_entity_type(entity_type, ucum_data, output_dir,
43
+ include_potential)
44
+ else
45
+ ENTITY_TYPES.each do |type|
46
+ process_entity_type(type, ucum_data, output_dir,
47
+ include_potential)
48
+ end
49
+ end
50
+
51
+ 0
52
+ end
53
+
54
+ private
55
+
56
+ def process_entity_type(entity_type, ucum_data, output_dir,
57
+ include_potential)
58
+ puts "\n========== Processing #{entity_type.upcase} References =========="
59
+
60
+ # Get entities
61
+ klass = Unitsdb.const_get(entity_type.capitalize)
62
+ yaml_path = File.join(@options[:database], "#{entity_type}.yaml")
63
+ entity_collection = klass.from_yaml(File.read(yaml_path))
64
+
65
+ ucum_entities = XmlParser.get_entities_from_ucum(entity_type,
66
+ ucum_data)
67
+
68
+ return if ucum_entities.nil? || ucum_entities.empty?
69
+
70
+ # Match entities
71
+ _, missing_refs, = Matcher.match_db_to_ucum(entity_type,
72
+ ucum_entities, entity_collection)
73
+
74
+ # Create output directory if it doesn't exist
75
+ FileUtils.mkdir_p(output_dir)
76
+
77
+ # Update references in UnitsDB entities
78
+ output_file = File.join(output_dir, "#{entity_type}.yaml")
79
+ Updater.update_references(entity_type, missing_refs,
80
+ entity_collection, output_file, include_potential)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module Unitsdb
7
+ module Commands
8
+ module Ucum
9
+ # Updater for adding UCUM references to UnitsDB entities
10
+ module Updater
11
+ UCUM_AUTHORITY = "ucum"
12
+
13
+ module_function
14
+
15
+ # Update references in UnitsDB entities with UCUM references
16
+ def update_references(entity_type, matches, db_entities, output_file,
17
+ include_potential = false)
18
+ puts "Updating UCUM references for #{entity_type}..."
19
+
20
+ # Create a map of entity IDs to their UCUM references
21
+ entity_references = {}
22
+
23
+ # Process each match
24
+ matches.each do |match|
25
+ db_entity = match[:db_entity]
26
+ ucum_entity = match[:ucum_entity]
27
+
28
+ # Skip potential matches unless specified
29
+ next if match[:potential] && !include_potential
30
+
31
+ # Get entity ID
32
+ entity_id = get_entity_id(db_entity)
33
+ next unless entity_id
34
+
35
+ # Initialize references for this entity
36
+ entity_references[entity_id] = ExternalReference.new(
37
+ uri: ucum_entity.identifier,
38
+ type: "informative",
39
+ authority: UCUM_AUTHORITY,
40
+ )
41
+ end
42
+
43
+ # Update the YAML content
44
+ db_entities.send(entity_type).each do |entity|
45
+ # Find entity by ID
46
+ entity_id = if entity.identifiers
47
+ begin
48
+ entity.identifiers.first.id
49
+ rescue StandardError
50
+ nil
51
+ end
52
+ end
53
+
54
+ next unless entity_id && entity_references.key?(entity_id)
55
+
56
+ # Initialize references array if it doesn't exist
57
+ entity.references ||= []
58
+
59
+ # Add new references
60
+ if (ext_ref = entity_references[entity_id])
61
+ if entity.references.detect do |ref|
62
+ ref.uri == ext_ref.uri && ref.authority == ext_ref.authority
63
+ end
64
+ # Skip if reference already exists
65
+ puts "Reference already exists for entity ID: #{entity_id}"
66
+ else
67
+ # Add the reference
68
+ puts "Adding reference for entity ID: #{entity_id}, URI: #{ext_ref.uri}, Authority: #{ext_ref.authority}"
69
+ entity.references << ext_ref
70
+ end
71
+ end
72
+ end
73
+
74
+ # Write to YAML file
75
+ write_yaml_file(output_file, db_entities)
76
+
77
+ puts "Added #{entity_references.values.flatten.size} UCUM references to #{entity_type}"
78
+ end
79
+
80
+ # Helper to write YAML file
81
+ def write_yaml_file(output_file, output_data)
82
+ # Ensure the output directory exists
83
+ output_dir = File.dirname(output_file)
84
+ FileUtils.mkdir_p(output_dir)
85
+
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
119
+ end
120
+
121
+ # Get entity ID (either from identifiers array or directly)
122
+ def get_entity_id(entity)
123
+ if entity.respond_to?(:identifiers) && entity.identifiers && !entity.identifiers.empty?
124
+ entity.identifiers.first.id
125
+ elsif entity.respond_to?(:id)
126
+ entity.id
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ module Commands
5
+ module Ucum
6
+ # Parser for UCUM XML files
7
+ module XmlParser
8
+ module_function
9
+
10
+ # Parse UCUM XML file and return parsed data
11
+ def parse_ucum_file(file_path)
12
+ puts "Parsing UCUM XML file: #{file_path}..."
13
+ content = File.read(file_path)
14
+ Unitsdb::UcumFile.from_xml(content)
15
+ end
16
+
17
+ # Get entities from parsed UCUM data based on entity type
18
+ def get_entities_from_ucum(entity_type, ucum_data)
19
+ case entity_type
20
+ when "prefixes"
21
+ ucum_data.prefixes
22
+ when "units"
23
+ # Combine base-units and units into a single array
24
+ ucum_data.base_units + ucum_data.units
25
+ else
26
+ []
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end