Module: Unitsdb::Commands::CheckSi::SiMatcher

Defined in:
lib/unitsdb/commands/check_si/si_matcher.rb

Overview

Matcher for SI digital-framework entities and UnitsDB entities. All iteration is typed — entities expose identifiers, names, short, references, and (for units/prefixes) symbols as declared Lutaml attributes, so we read them directly.

Constant Summary collapse

SI_AUTHORITY =
"si-digital-framework"
SYMBOL_ENTITY_TYPES =
%w[units prefixes].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.match_detailsObject

Returns the value of attribute match_details.



15
16
17
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 15

def match_details
  @match_details
end

Class Method Details

.find_entity_id(entity) ⇒ Object

UnitsDB top-level entities are identified by their first Identifier's id; if none is present, fall back to short.



188
189
190
191
192
193
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 188

def find_entity_id(entity)
  identifier = entity.identifiers.first
  return identifier.id if identifier&.id

  entity.short
end

.find_matching_entities(entity_type, ttl_entity, db_entities) ⇒ Object



200
201
202
203
204
205
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 200

def find_matching_entities(entity_type, ttl_entity, db_entities)
  finder = MATCHERS[entity_type]
  return [] unless finder

  finder.call(ttl_entity, db_entities)
end

.find_matching_prefixes(ttl_prefix, prefixes) ⇒ Object



225
226
227
228
229
230
231
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 225

def find_matching_prefixes(ttl_prefix, prefixes)
  prefixes.select do |prefix|
    short_matches?(prefix.short, ttl_prefix) ||
      name_matches?(prefix.names, ttl_prefix) ||
      prefix_symbol_matches?(prefix.symbols, ttl_prefix)
  end.uniq
end

.find_matching_quantities(ttl_quantity, quantities) ⇒ Object



218
219
220
221
222
223
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 218

def find_matching_quantities(ttl_quantity, quantities)
  quantities.select do |quantity|
    short_matches_any?(quantity.short, ttl_quantity, i[name label alt_label]) ||
      name_matches_any?(quantity.names, ttl_quantity, i[name label alt_label])
  end.uniq
end

.find_matching_units(ttl_unit, units) ⇒ Object

---- Per-entity-type matchers (open for extension: add to MATCHERS to support a new type) ----



210
211
212
213
214
215
216
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 210

def find_matching_units(ttl_unit, units)
  units.select do |unit|
    short_matches?(unit.short, ttl_unit) ||
      name_matches?(unit.names, ttl_unit) ||
      symbol_matches?(unit.symbols, ttl_unit)
  end.uniq
end

.format_entity_name(entity) ⇒ Object

First localized name (LocalizedString instance) or nil.



196
197
198
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 196

def format_entity_name(entity)
  entity.names.first
end

.match_db_to_ttl(entity_type, ttl_entities, db_entities) ⇒ Object

Match database entities to TTL entities (to_si direction)



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 121

def match_db_to_ttl(entity_type, ttl_entities, db_entities)
  matches = []
  missing_refs = []
  matched_db_ids = []
  processed_db_ids = {}

  nist_id_to_display = build_nist_id_to_display(entity_type, db_entities)

  db_entities.each do |db_entity|
    entity_id = find_entity_id(db_entity)
    display_id = nist_id_to_display[entity_id] || entity_id

    next if processed_db_ids[entity_id]

    processed_db_ids[entity_id] = true

    has_reference = false
    (db_entity.references || []).each do |ref|
      next unless ref.authority == SI_AUTHORITY

      has_reference = true
      ttl_entity = ttl_entities.find { |e| e[:uri] == ref.uri }
      matches << {
        entity_id: display_id,
        db_entity: db_entity,
        ttl_uri: ref.uri,
        ttl_entity: ttl_entity,
      }
    end

    if has_reference
      matched_db_ids << entity_id
      next
    end

    matching_ttl = []
    match_types = {}

    ttl_entities.each do |ttl_entity|
      match_result = match_entity_names?(entity_type, db_entity, ttl_entity)
      next unless match_result[:match]

      matching_ttl << ttl_entity
      match_types[ttl_entity[:uri]] = match_result[:match_type]
      match_details["#{entity_id}:#{ttl_entity[:uri]}"] = match_result
    end

    next if matching_ttl.empty?

    matched_db_ids << entity_id
    missing_refs << {
      entity_id: display_id,
      db_entity: db_entity,
      ttl_entities: matching_ttl,
      match_types: match_types,
    }
  end

  unmatched_db = db_entities.reject do |entity|
    matched_db_ids.include?(find_entity_id(entity))
  end

  [matches, missing_refs, unmatched_db]
end

.match_entity_names?(entity_type, db_entity, ttl_entity) ⇒ Boolean

---- Detailed match (returns a hash with match metadata) ----

Returns:

  • (Boolean)


275
276
277
278
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 275

def match_entity_names?(entity_type, db_entity, ttl_entity)
  matcher = DetailedMatcher.new(db_entity, ttl_entity, entity_type)
  matcher.call
end

.match_ttl_to_db(entity_type, ttl_entities, db_entities) ⇒ Object

Match TTL entities to database entities (from_si direction)



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 22

def match_ttl_to_db(entity_type, ttl_entities, db_entities)
  matches = []
  missing_matches = []
  matched_ttl_uris = []
  processed_pairs = {}
  entity_matches = {}

  db_entities.each do |entity|
    references = entity.references || []
    references.each do |ref|
      next unless ref.authority == SI_AUTHORITY

      matched_ttl_uris << ref.uri
      ttl_entity = ttl_entities.find { |e| e[:uri] == ref.uri }
      next unless ttl_entity

      matches << {
        entity_id: entity.short,
        entity_name: format_entity_name(entity),
        si_uri: ttl_entity[:uri],
        si_name: ttl_entity[:name],
        si_label: ttl_entity[:label],
        si_alt_label: ttl_entity[:alt_label],
        si_symbol: ttl_entity[:symbol],
        entity: entity,
      }
    end
  end

  ttl_entities.each do |ttl_entity|
    next if matched_ttl_uris.include?(ttl_entity[:uri])

    matching_entities = find_matching_entities(entity_type, ttl_entity, db_entities)
    next if matching_entities.empty?

    matched_ttl_uris << ttl_entity[:uri]

    matching_entities.each do |entity|
      entity_id = entity.short
      pair_key = "#{entity_id}:#{ttl_entity[:uri]}"
      next if processed_pairs[pair_key]

      processed_pairs[pair_key] = true

      match_result = match_entity_names?(entity_type, entity, ttl_entity)
      next unless match_result[:match]

      match_details[pair_key] = match_result

      has_reference = (entity.references || []).any? do |ref|
        ref.uri == ttl_entity[:uri] && ref.authority == SI_AUTHORITY
      end

      match_data = {
        entity_id: entity_id,
        entity_name: format_entity_name(entity),
        si_uri: ttl_entity[:uri],
        si_name: ttl_entity[:name],
        si_label: ttl_entity[:label],
        si_alt_label: ttl_entity[:alt_label],
        si_symbol: ttl_entity[:symbol],
        entity: entity,
        match_type: match_result[:match_type],
        match_details: match_result,
        match_types: { ttl_entity[:uri] => match_result[:match_type] },
      }

      if has_reference
        matches << match_data
      else
        entity_matches[entity_id] ||= []
        entity_matches[entity_id] << {
          uri: ttl_entity[:uri],
          name: ttl_entity[:name],
          label: ttl_entity[:label],
        }
        missing_matches << match_data unless missing_matches.any? { |m| m[:entity_id] == entity_id }
      end
    end
  end

  missing_matches.each do |match|
    si_matches = entity_matches[match[:entity_id]]
    next unless si_matches && si_matches.size > 1

    match[:multiple_si] = si_matches
  end

  unmatched_ttl = ttl_entities.reject do |entity|
    matched_ttl_uris.include?(entity[:uri]) ||
      entity[:uri].end_with?("/units/") ||
      entity[:uri].end_with?("/quantities/") ||
      entity[:uri].end_with?("/prefixes/")
  end

  [matches, missing_matches, unmatched_ttl]
end

.name_matches?(names, ttl_entity) ⇒ Boolean

Returns:

  • (Boolean)


252
253
254
255
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 252

def name_matches?(names, ttl_entity)
  targets = [ttl_entity[:name]&.downcase, ttl_entity[:label]&.downcase].compact
  names.any? { |n| targets.include?(n.value&.downcase) }
end

.name_matches_any?(names, ttl_entity, keys) ⇒ Boolean

Returns:

  • (Boolean)


257
258
259
260
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 257

def name_matches_any?(names, ttl_entity, keys)
  targets = keys.filter_map { |k| ttl_entity[k]&.downcase }
  names.any? { |n| targets.include?(n.value&.downcase) }
end

.short_matches?(short, ttl_entity) ⇒ Boolean

---- Generic match primitives ----

Returns:

  • (Boolean)


241
242
243
244
245
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 241

def short_matches?(short, ttl_entity)
  target = ttl_entity[:name]&.downcase
  target_label = ttl_entity[:label]&.downcase
  short && [target, target_label].include?(short.downcase)
end

.short_matches_any?(short, ttl_entity, keys) ⇒ Boolean

Returns:

  • (Boolean)


247
248
249
250
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 247

def short_matches_any?(short, ttl_entity, keys)
  targets = keys.map { |k| ttl_entity[k]&.downcase }
  short && targets.include?(short.downcase)
end

.symbol_matches?(symbols, ttl_entity) ⇒ Boolean Also known as: prefix_symbol_matches?

Returns:

  • (Boolean)


262
263
264
265
266
267
268
# File 'lib/unitsdb/commands/check_si/si_matcher.rb', line 262

def symbol_matches?(symbols, ttl_entity)
  ttl_symbol = ttl_entity[:symbol]
  return false unless ttl_symbol

  needle = ttl_symbol.downcase
  symbols.any? { |s| s.ascii.to_s.downcase == needle }
end