unitsdb 2.2.2 → 2.2.4

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/opal.yml +36 -0
  3. data/.gitignore +3 -0
  4. data/Gemfile +2 -1
  5. data/Rakefile +3 -1
  6. data/lib/unitsdb/cli.rb +5 -41
  7. data/lib/unitsdb/commands/_modify.rb +1 -34
  8. data/lib/unitsdb/commands/check_si/si_formatter.rb +6 -6
  9. data/lib/unitsdb/commands/check_si/si_matcher.rb +202 -292
  10. data/lib/unitsdb/commands/check_si/si_updater.rb +16 -36
  11. data/lib/unitsdb/commands/entity_presenter.rb +98 -0
  12. data/lib/unitsdb/commands/get.rb +16 -113
  13. data/lib/unitsdb/commands/qudt/formatter.rb +16 -27
  14. data/lib/unitsdb/commands/qudt/matcher.rb +18 -28
  15. data/lib/unitsdb/commands/qudt/updater.rb +8 -11
  16. data/lib/unitsdb/commands/qudt.rb +1 -34
  17. data/lib/unitsdb/commands/search.rb +33 -188
  18. data/lib/unitsdb/commands/thor.rb +41 -0
  19. data/lib/unitsdb/commands/ucum/formatter.rb +9 -18
  20. data/lib/unitsdb/commands/ucum/matcher.rb +4 -4
  21. data/lib/unitsdb/commands/ucum/updater.rb +3 -5
  22. data/lib/unitsdb/commands/ucum.rb +1 -34
  23. data/lib/unitsdb/commands/validate/qudt_references.rb +29 -70
  24. data/lib/unitsdb/commands/validate/references.rb +5 -303
  25. data/lib/unitsdb/commands/validate/si_references.rb +30 -66
  26. data/lib/unitsdb/commands/validate/ucum_references.rb +30 -64
  27. data/lib/unitsdb/commands/validate.rb +1 -36
  28. data/lib/unitsdb/commands.rb +2 -0
  29. data/lib/unitsdb/config.rb +83 -29
  30. data/lib/unitsdb/database/loader.rb +135 -0
  31. data/lib/unitsdb/database/reference_validator.rb +227 -0
  32. data/lib/unitsdb/database/uniqueness_validator.rb +80 -0
  33. data/lib/unitsdb/database.rb +124 -584
  34. data/lib/unitsdb/dimension.rb +0 -27
  35. data/lib/unitsdb/dimensions.rb +0 -2
  36. data/lib/unitsdb/opal.rb +43 -0
  37. data/lib/unitsdb/prefix.rb +0 -13
  38. data/lib/unitsdb/prefixes.rb +0 -2
  39. data/lib/unitsdb/quantities.rb +0 -1
  40. data/lib/unitsdb/quantity.rb +0 -2
  41. data/lib/unitsdb/quantity_reference.rb +0 -2
  42. data/lib/unitsdb/root_unit_reference.rb +0 -2
  43. data/lib/unitsdb/scale.rb +0 -2
  44. data/lib/unitsdb/scale_properties.rb +0 -1
  45. data/lib/unitsdb/scales.rb +0 -1
  46. data/lib/unitsdb/si_derived_base.rb +0 -1
  47. data/lib/unitsdb/symbol_presentations.rb +0 -2
  48. data/lib/unitsdb/unit.rb +0 -34
  49. data/lib/unitsdb/unit_system.rb +0 -2
  50. data/lib/unitsdb/unit_systems.rb +0 -2
  51. data/lib/unitsdb/units.rb +0 -2
  52. data/lib/unitsdb/version.rb +1 -1
  53. data/lib/unitsdb.rb +142 -35
  54. data/unitsdb.gemspec +1 -0
  55. metadata +23 -2
@@ -3,10 +3,18 @@
3
3
  module Unitsdb
4
4
  module Commands
5
5
  module CheckSi
6
- # Matcher for SI entities and UnitsDB entities
6
+ # Matcher for SI digital-framework entities and UnitsDB entities.
7
+ # All iteration is typed — entities expose `identifiers`, `names`,
8
+ # `short`, `references`, and (for units/prefixes) `symbols` as
9
+ # declared Lutaml attributes, so we read them directly.
7
10
  module SiMatcher
8
11
  SI_AUTHORITY = "si-digital-framework"
9
- @match_details = {}
12
+ SYMBOL_ENTITY_TYPES = %w[units prefixes].freeze
13
+
14
+ class << self
15
+ attr_accessor :match_details
16
+ end
17
+ self.match_details = {}
10
18
 
11
19
  module_function
12
20
 
@@ -15,14 +23,12 @@ module Unitsdb
15
23
  matches = []
16
24
  missing_matches = []
17
25
  matched_ttl_uris = []
18
- processed_pairs = {} # Track processed entity-ttl pairs to avoid duplicates
19
- entity_matches = {} # Track matches by entity ID
26
+ processed_pairs = {}
27
+ entity_matches = {}
20
28
 
21
- # First pass: find direct references
22
29
  db_entities.each do |entity|
23
- next unless entity.respond_to?(:references) && entity.references
24
-
25
- entity.references.each do |ref|
30
+ references = entity.references || []
31
+ references.each do |ref|
26
32
  next unless ref.authority == SI_AUTHORITY
27
33
 
28
34
  matched_ttl_uris << ref.uri
@@ -42,42 +48,33 @@ module Unitsdb
42
48
  end
43
49
  end
44
50
 
45
- # Second pass: find matching entities
46
51
  ttl_entities.each do |ttl_entity|
47
52
  next if matched_ttl_uris.include?(ttl_entity[:uri])
48
53
 
49
- matching_entities = find_matching_entities(entity_type, ttl_entity,
50
- db_entities)
54
+ matching_entities = find_matching_entities(entity_type, ttl_entity, db_entities)
51
55
  next if matching_entities.empty?
52
56
 
53
57
  matched_ttl_uris << ttl_entity[:uri]
54
58
 
55
59
  matching_entities.each do |entity|
56
60
  entity_id = entity.short
57
- entity_name = format_entity_name(entity)
58
-
59
- # Create a unique key for this entity-ttl pair to avoid duplicates
60
61
  pair_key = "#{entity_id}:#{ttl_entity[:uri]}"
61
62
  next if processed_pairs[pair_key]
62
63
 
63
64
  processed_pairs[pair_key] = true
64
65
 
65
- # Get detailed match information
66
- match_result = match_entity_names?(entity_type, entity,
67
- ttl_entity)
66
+ match_result = match_entity_names?(entity_type, entity, ttl_entity)
68
67
  next unless match_result[:match]
69
68
 
70
- # Save match details for later use
71
- @match_details[pair_key] = match_result
69
+ match_details[pair_key] = match_result
72
70
 
73
- # Check if already has reference
74
- has_reference = entity.references&.any? do |ref|
71
+ has_reference = (entity.references || []).any? do |ref|
75
72
  ref.uri == ttl_entity[:uri] && ref.authority == SI_AUTHORITY
76
73
  end
77
74
 
78
75
  match_data = {
79
76
  entity_id: entity_id,
80
- entity_name: entity_name,
77
+ entity_name: format_entity_name(entity),
81
78
  si_uri: ttl_entity[:uri],
82
79
  si_name: ttl_entity[:name],
83
80
  si_label: ttl_entity[:label],
@@ -92,35 +89,24 @@ module Unitsdb
92
89
  if has_reference
93
90
  matches << match_data
94
91
  else
95
- # Group by entity_id for multiple SI matches
96
92
  entity_matches[entity_id] ||= []
97
93
  entity_matches[entity_id] << {
98
94
  uri: ttl_entity[:uri],
99
95
  name: ttl_entity[:name],
100
96
  label: ttl_entity[:label],
101
97
  }
102
-
103
- # Add first occurrence of this entity to missing_matches
104
- missing_matches << match_data unless missing_matches.any? do |m|
105
- m[:entity_id] == entity_id
106
- end
98
+ missing_matches << match_data unless missing_matches.any? { |m| m[:entity_id] == entity_id }
107
99
  end
108
100
  end
109
101
  end
110
102
 
111
- # Update missing_matches to include multiple SI entities
112
103
  missing_matches.each do |match|
113
- entity_id = match[:entity_id]
114
- si_matches = entity_matches[entity_id]
104
+ si_matches = entity_matches[match[:entity_id]]
105
+ next unless si_matches && si_matches.size > 1
115
106
 
116
- # If entity matches multiple SI entities, record them
117
- if si_matches && si_matches.size > 1
118
- match[:multiple_si] =
119
- si_matches
120
- end
107
+ match[:multiple_si] = si_matches
121
108
  end
122
109
 
123
- # Find unmatched TTL entities
124
110
  unmatched_ttl = ttl_entities.reject do |entity|
125
111
  matched_ttl_uris.include?(entity[:uri]) ||
126
112
  entity[:uri].end_with?("/units/") ||
@@ -136,78 +122,49 @@ module Unitsdb
136
122
  matches = []
137
123
  missing_refs = []
138
124
  matched_db_ids = []
139
- processed_db_ids = {} # Track processed entities
125
+ processed_db_ids = {}
140
126
 
141
- # Map from NIST IDs to display names for original output compatibility
142
- nist_id_to_display = {}
143
-
144
- # Build mappings for each entity type
145
- db_entities.each do |entity|
146
- next unless entity.respond_to?(:identifiers) && entity.identifiers&.first&.id&.start_with?("NIST")
147
-
148
- nist_id = entity.identifiers.first.id
149
-
150
- # For quantities and prefixes, we want to show the "short" field
151
- nist_id_to_display[nist_id] = entity.short if %w[quantities
152
- prefixes].include?(entity_type) && entity.respond_to?(:short)
153
- end
127
+ nist_id_to_display = build_nist_id_to_display(entity_type, db_entities)
154
128
 
155
129
  db_entities.each do |db_entity|
156
130
  entity_id = find_entity_id(db_entity)
131
+ display_id = nist_id_to_display[entity_id] || entity_id
157
132
 
158
- # For display purposes - use original display names
159
- display_id = entity_id
160
-
161
- # Apply the NIST ID mapping if available
162
- display_id = nist_id_to_display[entity_id] if entity_id.start_with?("NIST") && nist_id_to_display[entity_id]
163
-
164
- # Skip if we've already processed this entity
165
133
  next if processed_db_ids[entity_id]
166
134
 
167
135
  processed_db_ids[entity_id] = true
168
- has_reference = false
169
136
 
170
- # Check for existing SI references
171
- if db_entity.respond_to?(:references) && db_entity.references
172
- db_entity.references.each do |ref|
173
- next unless ref.authority == SI_AUTHORITY
174
-
175
- has_reference = true
176
- # Find the matching TTL entity for display
177
- ttl_entity = ttl_entities.find { |e| e[:uri] == ref.uri }
137
+ has_reference = false
138
+ (db_entity.references || []).each do |ref|
139
+ next unless ref.authority == SI_AUTHORITY
178
140
 
179
- matches << {
180
- entity_id: display_id,
181
- db_entity: db_entity,
182
- ttl_uri: ref.uri,
183
- ttl_entity: ttl_entity,
184
- }
185
- end
141
+ has_reference = true
142
+ ttl_entity = ttl_entities.find { |e| e[:uri] == ref.uri }
143
+ matches << {
144
+ entity_id: display_id,
145
+ db_entity: db_entity,
146
+ ttl_uri: ref.uri,
147
+ ttl_entity: ttl_entity,
148
+ }
186
149
  end
187
150
 
188
- # If already has reference, continue to next entity
189
151
  if has_reference
190
152
  matched_db_ids << entity_id
191
153
  next
192
154
  end
193
155
 
194
- # Find matching TTL entities
195
156
  matching_ttl = []
196
157
  match_types = {}
197
158
 
198
159
  ttl_entities.each do |ttl_entity|
199
- match_result = match_entity_names?(entity_type, db_entity,
200
- ttl_entity)
160
+ match_result = match_entity_names?(entity_type, db_entity, ttl_entity)
201
161
  next unless match_result[:match]
202
162
 
203
163
  matching_ttl << ttl_entity
204
164
  match_types[ttl_entity[:uri]] = match_result[:match_type]
205
-
206
- # Save detailed match info
207
- @match_details["#{entity_id}:#{ttl_entity[:uri]}"] = match_result
165
+ match_details["#{entity_id}:#{ttl_entity[:uri]}"] = match_result
208
166
  end
209
167
 
210
- # If found matches, add to missing_refs
211
168
  next if matching_ttl.empty?
212
169
 
213
170
  matched_db_ids << entity_id
@@ -219,7 +176,6 @@ module Unitsdb
219
176
  }
220
177
  end
221
178
 
222
- # Find unmatched db entities
223
179
  unmatched_db = db_entities.reject do |entity|
224
180
  matched_db_ids.include?(find_entity_id(entity))
225
181
  end
@@ -227,260 +183,214 @@ module Unitsdb
227
183
  [matches, missing_refs, unmatched_db]
228
184
  end
229
185
 
230
- # Find entity ID
186
+ # UnitsDB top-level entities are identified by their first
187
+ # Identifier's id; if none is present, fall back to short.
231
188
  def find_entity_id(entity)
232
- return entity.id if entity.respond_to?(:id) && entity.id
233
- return entity.identifiers.first.id if entity.respond_to?(:identifiers) && !entity.identifiers.empty? &&
234
- entity.identifiers.first.respond_to?(:id)
189
+ identifier = entity.identifiers.first
190
+ return identifier.id if identifier&.id
235
191
 
236
192
  entity.short
237
193
  end
238
194
 
239
- # Format entity name correctly
195
+ # First localized name (LocalizedString instance) or nil.
240
196
  def format_entity_name(entity)
241
- return nil unless entity.respond_to?(:names) && entity.names&.first
242
-
243
197
  entity.names.first
244
-
245
- # # Special handling for sidereal names - use comma format
246
- # if name.include?("sidereal")
247
- # if name.start_with?("sidereal ")
248
- # # For names that already start with "sidereal " - strip it
249
- # base_name = name.gsub("sidereal ", "")
250
- # return "#{base_name}, sidereal"
251
- # elsif name.end_with?(" sidereal")
252
- # # For names that already have comma format but missing comma
253
- # parts = name.split
254
- # return "#{parts.first}, #{parts.last}"
255
- # end
256
- # end
257
-
258
- # # Handle other special cases
259
- # return name if name == "year (365 days)"
260
-
261
- # # Default to the original name
262
198
  end
263
199
 
264
- # Find matching entities for a TTL entity
265
200
  def find_matching_entities(entity_type, ttl_entity, db_entities)
266
- case entity_type
267
- when "units"
268
- find_matching_units(ttl_entity, db_entities)
269
- when "quantities"
270
- find_matching_quantities(ttl_entity, db_entities)
271
- when "prefixes"
272
- find_matching_prefixes(ttl_entity, db_entities)
273
- else
274
- []
275
- end
201
+ finder = MATCHERS[entity_type]
202
+ return [] unless finder
203
+
204
+ finder.call(ttl_entity, db_entities)
276
205
  end
277
206
 
278
- # Find exact matches for units
207
+ # ---- Per-entity-type matchers (open for extension: add to
208
+ # MATCHERS to support a new type) ----
209
+
279
210
  def find_matching_units(ttl_unit, units)
280
- matching_units = []
211
+ units.select do |unit|
212
+ short_matches?(unit.short, ttl_unit) ||
213
+ name_matches?(unit.names, ttl_unit) ||
214
+ symbol_matches?(unit.symbols, ttl_unit)
215
+ end.uniq
216
+ end
281
217
 
282
- units.each do |unit|
283
- # Match by short
284
- if unit.short&.downcase == ttl_unit[:name]&.downcase ||
285
- unit.short&.downcase == ttl_unit[:label]&.downcase
286
- matching_units << unit
287
- next
288
- end
218
+ def find_matching_quantities(ttl_quantity, quantities)
219
+ quantities.select do |quantity|
220
+ short_matches_any?(quantity.short, ttl_quantity, %i[name label alt_label]) ||
221
+ name_matches_any?(quantity.names, ttl_quantity, %i[name label alt_label])
222
+ end.uniq
223
+ end
289
224
 
290
- # Match by name
291
- if unit.respond_to?(:names) && unit.names&.any? do |name|
292
- name.downcase == ttl_unit[:name]&.downcase ||
293
- name.downcase == ttl_unit[:label]&.downcase
294
- end
295
- matching_units << unit
296
- next
297
- end
225
+ def find_matching_prefixes(ttl_prefix, prefixes)
226
+ prefixes.select do |prefix|
227
+ short_matches?(prefix.short, ttl_prefix) ||
228
+ name_matches?(prefix.names, ttl_prefix) ||
229
+ prefix_symbol_matches?(prefix.symbols, ttl_prefix)
230
+ end.uniq
231
+ end
298
232
 
299
- # Match by symbol
300
- next unless ttl_unit[:symbol] && unit.respond_to?(:symbols) && unit.symbols&.any? do |sym|
301
- sym.respond_to?(:ascii) && sym.ascii && sym.ascii.downcase == ttl_unit[:symbol].downcase
302
- end
233
+ MATCHERS = {
234
+ "units" => method(:find_matching_units),
235
+ "quantities" => method(:find_matching_quantities),
236
+ "prefixes" => method(:find_matching_prefixes),
237
+ }.freeze
303
238
 
304
- matching_units << unit
305
- end
239
+ # ---- Generic match primitives ----
306
240
 
307
- matching_units.uniq
241
+ def short_matches?(short, ttl_entity)
242
+ target = ttl_entity[:name]&.downcase
243
+ target_label = ttl_entity[:label]&.downcase
244
+ short && [target, target_label].include?(short.downcase)
308
245
  end
309
246
 
310
- # Find exact matches for quantities
311
- def find_matching_quantities(ttl_quantity, quantities)
312
- matching_quantities = []
313
-
314
- quantities.each do |quantity|
315
- # Match by short
316
- if quantity.short&.downcase == ttl_quantity[:name]&.downcase ||
317
- quantity.short&.downcase == ttl_quantity[:label]&.downcase ||
318
- quantity.short&.downcase == ttl_quantity[:alt_label]&.downcase
319
- matching_quantities << quantity
320
- next
321
- end
247
+ def short_matches_any?(short, ttl_entity, keys)
248
+ targets = keys.map { |k| ttl_entity[k]&.downcase }
249
+ short && targets.include?(short.downcase)
250
+ end
322
251
 
323
- # Match by name
324
- next unless quantity.respond_to?(:names) && quantity.names&.any? do |name|
325
- name.downcase == ttl_quantity[:name]&.downcase ||
326
- name.downcase == ttl_quantity[:label]&.downcase ||
327
- name.downcase == ttl_quantity[:alt_label]&.downcase
328
- end
252
+ def name_matches?(names, ttl_entity)
253
+ targets = [ttl_entity[:name]&.downcase, ttl_entity[:label]&.downcase].compact
254
+ names.any? { |n| targets.include?(n.value&.downcase) }
255
+ end
329
256
 
330
- matching_quantities << quantity
331
- end
257
+ def name_matches_any?(names, ttl_entity, keys)
258
+ targets = keys.filter_map { |k| ttl_entity[k]&.downcase }
259
+ names.any? { |n| targets.include?(n.value&.downcase) }
260
+ end
261
+
262
+ def symbol_matches?(symbols, ttl_entity)
263
+ ttl_symbol = ttl_entity[:symbol]
264
+ return false unless ttl_symbol
332
265
 
333
- matching_quantities.uniq
266
+ needle = ttl_symbol.downcase
267
+ symbols.any? { |s| s.ascii.to_s.downcase == needle }
334
268
  end
335
269
 
336
- # Find exact matches for prefixes
337
- def find_matching_prefixes(ttl_prefix, prefixes)
338
- matching_prefixes = []
270
+ # Prefixes in 2.0 carry a `symbols` collection, just like Units.
271
+ alias prefix_symbol_matches? symbol_matches?
339
272
 
340
- prefixes.each do |prefix|
341
- # Match by short
342
- if prefix.short&.downcase == ttl_prefix[:name]&.downcase ||
343
- prefix.short&.downcase == ttl_prefix[:label]&.downcase
344
- matching_prefixes << prefix
345
- next
346
- end
273
+ # ---- Detailed match (returns a hash with match metadata) ----
347
274
 
348
- # Match by name
349
- if prefix.respond_to?(:names) && prefix.names&.any? do |name|
350
- name.downcase == ttl_prefix[:name]&.downcase ||
351
- name.downcase == ttl_prefix[:label]&.downcase
352
- end
353
- matching_prefixes << prefix
354
- next
355
- end
275
+ def match_entity_names?(entity_type, db_entity, ttl_entity)
276
+ matcher = DetailedMatcher.new(db_entity, ttl_entity, entity_type)
277
+ matcher.call
278
+ end
356
279
 
357
- # Match by symbol
358
- next unless ttl_prefix[:symbol] && prefix.respond_to?(:symbol) && prefix.symbol &&
359
- prefix.symbol.respond_to?(:ascii) && prefix.symbol.ascii &&
360
- prefix.symbol.ascii.downcase == ttl_prefix[:symbol].downcase
280
+ # Encapsulates the per-entity detailed match strategies.
281
+ class DetailedMatcher
282
+ def initialize(db_entity, ttl_entity, entity_type)
283
+ @db_entity = db_entity
284
+ @ttl = ttl_entity
285
+ @entity_type = entity_type
286
+ end
361
287
 
362
- matching_prefixes << prefix
288
+ def call
289
+ short_to_name || short_to_label || name_to_name ||
290
+ name_to_label || name_to_alt_label || sidereal_demotion ||
291
+ symbol_potential || NO_MATCH
363
292
  end
364
293
 
365
- matching_prefixes.uniq
366
- end
294
+ NO_MATCH = { match: false }.freeze
367
295
 
368
- # Match entity names with detailed type information
369
- def match_entity_names?(entity_type, db_entity, ttl_entity)
370
- match_details = { match: false }
296
+ private
371
297
 
372
- # Match by short name - EXACT match
373
- if db_entity.short && db_entity.short.downcase == ttl_entity[:name].downcase
374
- match_details = {
375
- match: true,
376
- exact: true,
377
- match_type: "Exact match",
378
- match_desc: "short_to_name",
379
- details: "UnitsDB short '#{db_entity.short}' matches SI name '#{ttl_entity[:name]}'",
380
- }
381
- # Match by short to label
382
- elsif db_entity.short && ttl_entity[:label] && db_entity.short.downcase == ttl_entity[:label].downcase
383
- match_details = {
298
+ def short_to_name
299
+ return unless @db_entity.short&.downcase == @ttl[:name]&.downcase
300
+
301
+ exact_match("short_to_name",
302
+ "UnitsDB short '#{@db_entity.short}' matches SI name '#{@ttl[:name]}'")
303
+ end
304
+
305
+ def short_to_label
306
+ return unless @db_entity.short && @ttl[:label] &&
307
+ @db_entity.short.downcase == @ttl[:label].downcase
308
+
309
+ exact_match("short_to_label",
310
+ "UnitsDB short '#{@db_entity.short}' matches SI label '#{@ttl[:label]}'")
311
+ end
312
+
313
+ def name_to_name
314
+ db_name = find_name_match(@ttl[:name])
315
+ return unless db_name
316
+
317
+ exact_match("name_to_name",
318
+ "UnitsDB name '#{db_name}' matches SI name '#{@ttl[:name]}'")
319
+ end
320
+
321
+ def name_to_label
322
+ return unless @ttl[:label]
323
+
324
+ db_name = find_name_match(@ttl[:label])
325
+ return unless db_name
326
+
327
+ exact_match("name_to_label",
328
+ "UnitsDB name '#{db_name}' matches SI label '#{@ttl[:label]}'")
329
+ end
330
+
331
+ def name_to_alt_label
332
+ return unless @ttl[:alt_label]
333
+
334
+ db_name = find_name_match(@ttl[:alt_label])
335
+ return unless db_name
336
+
337
+ exact_match("name_to_alt_label",
338
+ "UnitsDB name '#{db_name}' matches SI alt_label '#{@ttl[:alt_label]}'")
339
+ end
340
+
341
+ # A `sidereal_*` short counts as a partial match unless the
342
+ # TTL name/label acknowledges the sidereal form.
343
+ def sidereal_demotion
344
+ prior = short_to_name || short_to_label
345
+ return unless prior && prior[:exact]
346
+ return unless @db_entity.short&.include?("sidereal_")
347
+ return if @ttl[:name]&.include?("sidereal") || @ttl[:label]&.include?("sidereal")
348
+
349
+ potential_match("partial_match",
350
+ "UnitsDB '#{@db_entity.short}' partially matches SI '#{@ttl[:name]}'")
351
+ end
352
+
353
+ def symbol_potential
354
+ return unless SYMBOL_ENTITY_TYPES.include?(@entity_type)
355
+ return unless @ttl[:symbol]
356
+
357
+ needle = @ttl[:symbol].downcase
358
+ match = @db_entity.symbols.find { |s| s.ascii.to_s.downcase == needle }
359
+ return unless match
360
+
361
+ potential_match("symbol_match",
362
+ "UnitsDB symbol '#{match.ascii}' matches SI symbol '#{@ttl[:symbol]}'")
363
+ end
364
+
365
+ def find_name_match(ttl_value)
366
+ return unless ttl_value
367
+
368
+ needle = ttl_value.downcase
369
+ @db_entity.names.find { |n| n.value&.downcase == needle }
370
+ end
371
+
372
+ def exact_match(desc, details)
373
+ {
384
374
  match: true,
385
375
  exact: true,
386
376
  match_type: "Exact match",
387
- match_desc: "short_to_label",
388
- details: "UnitsDB short '#{db_entity.short}' matches SI label '#{ttl_entity[:label]}'",
377
+ match_desc: desc,
378
+ details: details,
389
379
  }
390
- # Match by names - EXACT match
391
- elsif db_entity.respond_to?(:names) && db_entity.names
392
- # Match by TTL name
393
- db_name_match = db_entity.names.find do |name|
394
- name.downcase == ttl_entity[:name].downcase
395
- end
396
- if db_name_match
397
- match_details = {
398
- match: true,
399
- exact: true,
400
- match_type: "Exact match",
401
- match_desc: "name_to_name",
402
- details: "UnitsDB name '#{db_name_match}' matches SI name '#{ttl_entity[:name]}'",
403
- }
404
- # Match by TTL label
405
- elsif ttl_entity[:label]
406
- db_name_match = db_entity.names.find do |name|
407
- name.downcase == ttl_entity[:label].downcase
408
- end
409
- if db_name_match
410
- match_details = {
411
- match: true,
412
- exact: true,
413
- match_type: "Exact match",
414
- match_desc: "name_to_label",
415
- details: "UnitsDB name '#{db_name_match}' matches SI label '#{ttl_entity[:label]}'",
416
- }
417
- end
418
- end
419
-
420
- # Match by TTL alt_label
421
- if !match_details[:match] && ttl_entity[:alt_label]
422
- db_name_match = db_entity.names.find do |name|
423
- name.downcase == ttl_entity[:alt_label].downcase
424
- end
425
- if db_name_match
426
- match_details = {
427
- match: true,
428
- exact: true,
429
- match_type: "Exact match",
430
- match_desc: "name_to_alt_label",
431
- details: "UnitsDB name '#{db_name_match}' matches SI alt_label '#{ttl_entity[:alt_label]}'",
432
- }
433
- end
434
- end
435
380
  end
436
381
 
437
- # Special validation for "sidereal_" units
438
- if match_details[:match] && match_details[:exact] && db_entity.short&.include?("sidereal_") &&
439
- !(ttl_entity[:name]&.include?("sidereal") || ttl_entity[:label]&.include?("sidereal"))
440
- match_details = {
382
+ def potential_match(desc, details)
383
+ {
441
384
  match: true,
442
385
  exact: false,
443
386
  match_type: "Potential match",
444
- match_desc: "partial_match",
445
- details: "UnitsDB '#{db_entity.short}' partially matches SI '#{ttl_entity[:name]}'",
387
+ match_desc: desc,
388
+ details: details,
446
389
  }
447
390
  end
448
-
449
- # Match by symbol if available (units and prefixes) - POTENTIAL match
450
- if !match_details[:match] && %w[units
451
- prefixes].include?(entity_type) && ttl_entity[:symbol]
452
- if entity_type == "units" && db_entity.respond_to?(:symbols) && db_entity.symbols
453
- matching_symbol = db_entity.symbols.find do |sym|
454
- sym.respond_to?(:ascii) && sym.ascii && sym.ascii.downcase == ttl_entity[:symbol].downcase
455
- end
456
-
457
- if matching_symbol
458
- match_details = {
459
- match: true,
460
- exact: false,
461
- match_type: "Potential match",
462
- match_desc: "symbol_match",
463
- details: "UnitsDB symbol '#{matching_symbol.ascii}' matches SI symbol '#{ttl_entity[:symbol]}'",
464
- }
465
- end
466
- elsif entity_type == "prefixes" && db_entity.respond_to?(:symbol) && db_entity.symbol
467
- if db_entity.symbol.respond_to?(:ascii) &&
468
- db_entity.symbol.ascii &&
469
- db_entity.symbol.ascii.downcase == ttl_entity[:symbol].downcase
470
-
471
- match_details = {
472
- match: true,
473
- exact: false,
474
- match_type: "Potential match",
475
- match_desc: "symbol_match",
476
- details: "UnitsDB symbol '#{db_entity.symbol.ascii}' matches SI symbol '#{ttl_entity[:symbol]}'",
477
- }
478
- end
479
- end
480
- end
481
-
482
- match_details
483
391
  end
392
+
393
+ private_constant :DetailedMatcher, :MATCHERS
484
394
  end
485
395
  end
486
396
  end