unitsdb 0.1.1 → 2.1.0

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +5 -0
  3. data/.github/workflows/depenedent-gems.yml +16 -0
  4. data/.gitmodules +3 -0
  5. data/.rspec +2 -1
  6. data/.rubocop_todo.yml +88 -12
  7. data/Gemfile +2 -2
  8. data/README.adoc +697 -1
  9. data/exe/unitsdb +7 -0
  10. data/lib/unitsdb/cli.rb +81 -0
  11. data/lib/unitsdb/commands/_modify.rb +22 -0
  12. data/lib/unitsdb/commands/base.rb +52 -0
  13. data/lib/unitsdb/commands/check_si.rb +124 -0
  14. data/lib/unitsdb/commands/get.rb +133 -0
  15. data/lib/unitsdb/commands/normalize.rb +81 -0
  16. data/lib/unitsdb/commands/release.rb +112 -0
  17. data/lib/unitsdb/commands/search.rb +219 -0
  18. data/lib/unitsdb/commands/si_formatter.rb +485 -0
  19. data/lib/unitsdb/commands/si_matcher.rb +470 -0
  20. data/lib/unitsdb/commands/si_ttl_parser.rb +100 -0
  21. data/lib/unitsdb/commands/si_updater.rb +212 -0
  22. data/lib/unitsdb/commands/validate/identifiers.rb +40 -0
  23. data/lib/unitsdb/commands/validate/references.rb +316 -0
  24. data/lib/unitsdb/commands/validate/si_references.rb +115 -0
  25. data/lib/unitsdb/commands/validate.rb +40 -0
  26. data/lib/unitsdb/config.rb +19 -0
  27. data/lib/unitsdb/database.rb +661 -0
  28. data/lib/unitsdb/dimension.rb +19 -25
  29. data/lib/unitsdb/dimension_details.rb +20 -0
  30. data/lib/unitsdb/dimension_reference.rb +8 -0
  31. data/lib/unitsdb/dimensions.rb +3 -6
  32. data/lib/unitsdb/errors.rb +13 -0
  33. data/lib/unitsdb/external_reference.rb +14 -0
  34. data/lib/unitsdb/identifier.rb +8 -0
  35. data/lib/unitsdb/localized_string.rb +17 -0
  36. data/lib/unitsdb/prefix.rb +11 -12
  37. data/lib/unitsdb/prefix_reference.rb +10 -0
  38. data/lib/unitsdb/prefixes.rb +3 -6
  39. data/lib/unitsdb/quantities.rb +3 -27
  40. data/lib/unitsdb/quantity.rb +12 -24
  41. data/lib/unitsdb/quantity_reference.rb +4 -7
  42. data/lib/unitsdb/root_unit_reference.rb +14 -0
  43. data/lib/unitsdb/scale.rb +17 -0
  44. data/lib/unitsdb/scale_properties.rb +12 -0
  45. data/lib/unitsdb/scale_reference.rb +10 -0
  46. data/lib/unitsdb/scales.rb +11 -0
  47. data/lib/unitsdb/si_derived_base.rb +13 -14
  48. data/lib/unitsdb/symbol_presentations.rb +14 -0
  49. data/lib/unitsdb/unit.rb +20 -26
  50. data/lib/unitsdb/unit_reference.rb +5 -8
  51. data/lib/unitsdb/unit_system.rb +8 -10
  52. data/lib/unitsdb/unit_system_reference.rb +10 -0
  53. data/lib/unitsdb/unit_systems.rb +3 -16
  54. data/lib/unitsdb/units.rb +3 -6
  55. data/lib/unitsdb/utils.rb +84 -0
  56. data/lib/unitsdb/version.rb +1 -1
  57. data/lib/unitsdb.rb +13 -10
  58. data/unitsdb.gemspec +6 -1
  59. metadata +112 -12
  60. data/lib/unitsdb/dimension_quantity.rb +0 -28
  61. data/lib/unitsdb/dimension_symbol.rb +0 -22
  62. data/lib/unitsdb/prefix_symbol.rb +0 -12
  63. data/lib/unitsdb/root_unit.rb +0 -17
  64. data/lib/unitsdb/root_units.rb +0 -20
  65. data/lib/unitsdb/symbol.rb +0 -17
  66. data/lib/unitsdb/unit_symbol.rb +0 -15
  67. data/lib/unitsdb/unitsdb.rb +0 -6
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require "json"
5
+ require_relative "../errors"
6
+
7
+ module Unitsdb
8
+ module Commands
9
+ class Search < Base
10
+ def run(query)
11
+ # Database path is guaranteed by Thor's global option
12
+
13
+ type = @options[:type]
14
+ id = @options[:id]
15
+ id_type = @options[:id_type]
16
+ format = @options[:format] || "text"
17
+
18
+ begin
19
+ database = load_database(@options[:database])
20
+
21
+ # Search by ID (early return)
22
+ if id
23
+ entity = database.get_by_id(id: id, type: id_type)
24
+
25
+ unless entity
26
+ puts "No entity found with ID: '#{id}'"
27
+ return
28
+ end
29
+
30
+ # Use the same output logic as the Get command
31
+ if %w[json yaml].include?(format.downcase)
32
+ begin
33
+ puts entity.send("to_#{format.downcase}")
34
+ return
35
+ rescue NoMethodError
36
+ puts "Error: Unable to convert entity to #{format} format"
37
+ exit(1)
38
+ end
39
+ end
40
+
41
+ print_entity_details(entity)
42
+ return
43
+ end
44
+
45
+ # Regular text search
46
+ results = database.search(text: query, type: type)
47
+
48
+ # Early return for empty results
49
+ if results.empty?
50
+ puts "No results found for '#{query}'"
51
+ return
52
+ end
53
+
54
+ # Format-specific output
55
+ if %w[json yaml].include?(format.downcase)
56
+ temp_db = create_temporary_database(results)
57
+ puts temp_db.send("to_#{format.downcase}")
58
+ return
59
+ end
60
+
61
+ # Default text output
62
+ puts "Found #{results.size} result(s) for '#{query}':"
63
+ results.each do |entity|
64
+ print_entity_with_ids(entity)
65
+ end
66
+ rescue Unitsdb::Errors::DatabaseError => e
67
+ puts "Error: #{e.message}"
68
+ exit(1)
69
+ rescue StandardError => e
70
+ puts "Error searching database: #{e.message}"
71
+ exit(1)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def print_entity_with_ids(entity)
78
+ # Determine entity type
79
+ entity_type = get_entity_type(entity)
80
+
81
+ # Get name
82
+ name = get_entity_name(entity)
83
+
84
+ # Get all identifiers
85
+ identifiers = entity.identifiers || []
86
+
87
+ # Print entity information
88
+ puts " - #{entity_type}: #{name}"
89
+
90
+ # Print each identifier on its own line for better readability
91
+ if identifiers.empty?
92
+ puts " ID: None"
93
+ else
94
+ puts " IDs:"
95
+ identifiers.each do |id|
96
+ puts " - #{id.id} (Type: #{id.type || "N/A"})"
97
+ end
98
+ end
99
+
100
+ # If entity has a short description, print it
101
+ puts " Description: #{entity.short}" if entity.respond_to?(:short) && entity.short && entity.short != name
102
+
103
+ # Add a blank line for readability
104
+ puts ""
105
+ end
106
+
107
+ def print_entity_details(entity)
108
+ # Determine entity type
109
+ entity_type = get_entity_type(entity)
110
+
111
+ # Get name
112
+ name = get_entity_name(entity)
113
+
114
+ puts "Entity details:"
115
+ puts " - Type: #{entity_type}"
116
+ puts " - Name: #{name}"
117
+
118
+ # Print description if available
119
+ puts " - Description: #{entity.short}" if entity.respond_to?(:short) && entity.short && entity.short != name
120
+
121
+ # Print all identifiers
122
+ if entity.identifiers&.any?
123
+ puts " - Identifiers:"
124
+ entity.identifiers.each do |id|
125
+ puts " - #{id.id} (Type: #{id.type || "N/A"})"
126
+ end
127
+ else
128
+ puts " - Identifiers: None"
129
+ end
130
+
131
+ # Print additional properties based on entity type
132
+ case entity
133
+ when Unitsdb::Unit
134
+ 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?
136
+
137
+ puts " - Definition: #{entity.definition}" if entity.respond_to?(:definition) && entity.definition
138
+
139
+ if entity.respond_to?(:dimensions) && entity.dimensions&.any?
140
+ puts " - Dimensions:"
141
+ entity.dimensions.each { |d| puts " - #{d}" }
142
+ end
143
+ when Unitsdb::Quantity
144
+ puts " - Dimensions: #{entity.dimension}" if entity.respond_to?(:dimension) && entity.dimension
145
+ when Unitsdb::Prefix
146
+ puts " - Value: #{entity.value}" if entity.respond_to?(:value) && entity.value
147
+ puts " - Symbol: #{entity.symbol}" if entity.respond_to?(:symbol) && entity.symbol
148
+ when Unitsdb::Dimension
149
+ # Any dimension-specific properties
150
+ when Unitsdb::UnitSystem
151
+ puts " - Organization: #{entity.organization}" if entity.respond_to?(:organization) && entity.organization
152
+ end
153
+
154
+ # Print references if available
155
+ return unless entity.respond_to?(:references) && entity.references&.any?
156
+
157
+ puts " - References:"
158
+ entity.references.each do |ref|
159
+ puts " - #{ref.type}: #{ref.id}"
160
+ end
161
+ end
162
+
163
+ def get_entity_type(entity)
164
+ case entity
165
+ when Unitsdb::Unit
166
+ "Unit"
167
+ when Unitsdb::Prefix
168
+ "Prefix"
169
+ when Unitsdb::Quantity
170
+ "Quantity"
171
+ when Unitsdb::Dimension
172
+ "Dimension"
173
+ when Unitsdb::UnitSystem
174
+ "UnitSystem"
175
+ else
176
+ "Unknown"
177
+ end
178
+ end
179
+
180
+ def get_entity_name(entity)
181
+ # Using early returns is still preferable for simple conditions
182
+ return entity.names.first if entity.respond_to?(:names) && entity.names&.any?
183
+ return entity.name if entity.respond_to?(:name) && entity.name
184
+ return entity.short if entity.respond_to?(:short) && entity.short
185
+
186
+ "N/A" # Default if no name found
187
+ end
188
+
189
+ def create_temporary_database(results)
190
+ temp_db = Unitsdb::Database.new
191
+
192
+ # Initialize collections
193
+ temp_db.units = []
194
+ temp_db.prefixes = []
195
+ temp_db.quantities = []
196
+ temp_db.dimensions = []
197
+ temp_db.unit_systems = []
198
+
199
+ # Add results to appropriate collection based on type using case statement
200
+ results.each do |entity|
201
+ case entity
202
+ when Unitsdb::Unit
203
+ temp_db.units << entity
204
+ when Unitsdb::Prefix
205
+ temp_db.prefixes << entity
206
+ when Unitsdb::Quantity
207
+ temp_db.quantities << entity
208
+ when Unitsdb::Dimension
209
+ temp_db.dimensions << entity
210
+ when Unitsdb::UnitSystem
211
+ temp_db.unit_systems << entity
212
+ end
213
+ end
214
+
215
+ temp_db
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,485 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "terminal-table"
4
+ require_relative "si_ttl_parser"
5
+
6
+ module Unitsdb
7
+ module Commands
8
+ # Formatter for SI check results
9
+ module SiFormatter
10
+ module_function
11
+
12
+ # Display TTL → DB results
13
+ def display_si_results(entity_type, matches, missing_matches, unmatched_ttl)
14
+ puts "\n=== #{entity_type.capitalize} with matching SI references ==="
15
+ if matches.empty?
16
+ puts "None"
17
+ else
18
+ rows = []
19
+ matches.each do |match|
20
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:si_uri])
21
+ rows << [
22
+ "UnitsDB: #{match[:entity_id]}",
23
+ "(#{match[:entity_name] || "unnamed"})"
24
+ ]
25
+ rows << [
26
+ "SI TTL: #{si_suffix}",
27
+ "(#{match[:si_label] || match[:si_name] || "unnamed"})"
28
+ ]
29
+ rows << :separator unless match == matches.last
30
+ end
31
+
32
+ table = Terminal::Table.new(
33
+ title: "Valid SI Reference Mappings",
34
+ rows: rows
35
+ )
36
+ puts table
37
+ end
38
+
39
+ puts "\n=== #{entity_type.capitalize} without SI references ==="
40
+ if missing_matches.empty?
41
+ puts "None"
42
+ else
43
+ # Split matches into exact and potential
44
+ exact_matches = []
45
+ potential_matches = []
46
+
47
+ missing_matches.each do |match|
48
+ # Get match details
49
+ match_details = match[:match_details]
50
+ match_desc = match_details&.dig(:match_desc) || ""
51
+
52
+ # Symbol matches and partial matches should always be potential matches
53
+ if %w[symbol_match partial_match].include?(match_desc)
54
+ potential_matches << match
55
+ elsif match_details&.dig(:exact) == false
56
+ potential_matches << match
57
+ else
58
+ exact_matches << match
59
+ end
60
+ end
61
+
62
+ # Display exact matches
63
+ puts "\n=== Exact Matches (#{exact_matches.size}) ==="
64
+ if exact_matches.empty?
65
+ puts "None"
66
+ else
67
+ rows = []
68
+ exact_matches.each do |match|
69
+ # First row: UnitsDB entity
70
+ rows << [
71
+ "UnitsDB: #{match[:entity_id]}",
72
+ "(#{match[:entity_name] || "unnamed"})"
73
+ ]
74
+
75
+ # Handle multiple SI matches in a single cell if present
76
+ if match[:multiple_si]
77
+ # Ensure no duplicate URIs
78
+ si_text_parts = []
79
+ si_label_parts = []
80
+ seen_uris = {}
81
+
82
+ match[:multiple_si].each do |si_data|
83
+ uri = si_data[:uri]
84
+ next if seen_uris[uri] # Skip if we've already seen this URI
85
+
86
+ seen_uris[uri] = true
87
+
88
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
89
+ si_text_parts << suffix
90
+ si_label_parts << (si_data[:label] || si_data[:name])
91
+ end
92
+
93
+ rows << [
94
+ "SI TTL: #{si_text_parts.join(", ")}",
95
+ "(#{si_label_parts.join(", ")})"
96
+ ]
97
+ else
98
+ # Second row: SI TTL suffix and label/name
99
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:si_uri])
100
+ rows << [
101
+ "SI TTL: #{si_suffix}",
102
+ "(#{match[:si_label] || match[:si_name] || "unnamed"})"
103
+ ]
104
+ end
105
+
106
+ # Status line with match type
107
+ match_details = match[:match_details]
108
+ match_desc = match_details&.dig(:match_desc) || ""
109
+ match_info = format_match_info(match_desc)
110
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference (#{match_info})"
111
+
112
+ rows << [
113
+ "Status: #{status_text}",
114
+ "✗"
115
+ ]
116
+ rows << :separator unless match == exact_matches.last
117
+ end
118
+
119
+ table = Terminal::Table.new(
120
+ title: "Exact Match Missing SI References",
121
+ rows: rows
122
+ )
123
+ puts table
124
+ end
125
+
126
+ # Display potential matches
127
+ puts "\n=== Potential Matches (#{potential_matches.size}) ==="
128
+ if potential_matches.empty?
129
+ puts "None"
130
+ else
131
+ rows = []
132
+ potential_matches.each do |match|
133
+ # First row: UnitsDB entity
134
+ rows << [
135
+ "UnitsDB: #{match[:entity_id]}",
136
+ "(#{match[:entity_name] || "unnamed"})"
137
+ ]
138
+
139
+ # Handle multiple SI matches in a single cell if present
140
+ if match[:multiple_si]
141
+ # Ensure no duplicate URIs
142
+ si_text_parts = []
143
+ seen_uris = {}
144
+
145
+ match[:multiple_si].each do |si_data|
146
+ uri = si_data[:uri]
147
+ next if seen_uris[uri] # Skip if we've already seen this URI
148
+
149
+ seen_uris[uri] = true
150
+
151
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
152
+ si_text_parts << "#{suffix} (#{si_data[:label] || si_data[:name]})"
153
+ end
154
+
155
+ rows << [
156
+ "SI TTL: #{si_text_parts.join(", ")}",
157
+ ""
158
+ ]
159
+ else
160
+ # Single TTL entity
161
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:si_uri])
162
+ rows << [
163
+ "SI TTL: #{si_suffix}",
164
+ "(#{match[:si_label] || match[:si_name] || "unnamed"})"
165
+ ]
166
+ end
167
+
168
+ # Status line
169
+ match_details = match[:match_details]
170
+ match_desc = match_details&.dig(:match_desc) || ""
171
+ match_info = format_match_info(match_desc)
172
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference"
173
+
174
+ rows << [
175
+ "Status: #{status_text}",
176
+ "✗"
177
+ ]
178
+ rows << :separator unless match == potential_matches.last
179
+ end
180
+
181
+ table = Terminal::Table.new(
182
+ title: "Potential Match Missing SI References",
183
+ rows: rows
184
+ )
185
+ puts table
186
+ end
187
+ end
188
+
189
+ puts "\n=== SI #{entity_type.capitalize} not mapped to our database ==="
190
+ if unmatched_ttl.empty?
191
+ puts "None (All TTL entities are referenced - Good job!)"
192
+ else
193
+ # Group unmatched ttl entities by their URI to avoid duplicates
194
+ grouped_unmatched = {}
195
+
196
+ unmatched_ttl.each do |entity|
197
+ uri = entity[:uri]
198
+ grouped_unmatched[uri] = entity unless grouped_unmatched.key?(uri)
199
+ end
200
+
201
+ rows = []
202
+ unique_entities = grouped_unmatched.values
203
+
204
+ unique_entities.each do |entity|
205
+ # Create the SI TTL row
206
+ si_suffix = SiTtlParser.extract_identifying_suffix(entity[:uri])
207
+ ttl_row = ["SI TTL: #{si_suffix}", "(#{entity[:label] || entity[:name] || "unnamed"})"]
208
+
209
+ rows << ttl_row
210
+ rows << [
211
+ "Status: No matching UnitsDB entity",
212
+ "?"
213
+ ]
214
+ rows << :separator unless entity == unique_entities.last
215
+ end
216
+
217
+ table = Terminal::Table.new(
218
+ title: "Unmapped SI Entities",
219
+ rows: rows
220
+ )
221
+ puts table
222
+ end
223
+ end
224
+
225
+ # Display DB → TTL results
226
+ def display_db_results(entity_type, matches, missing_refs, unmatched_db)
227
+ puts "\n=== Summary of database entities referencing SI ==="
228
+ puts "#{entity_type.capitalize} with SI references: #{matches.size}"
229
+ puts "#{entity_type.capitalize} missing SI references: #{missing_refs.size}"
230
+ puts "Database #{entity_type} not matching any SI entity: #{unmatched_db.size}"
231
+
232
+ # Show entities with valid references
233
+ unless matches.empty?
234
+ puts "\n=== #{entity_type.capitalize} with SI references ==="
235
+ rows = []
236
+ matches.each do |match|
237
+ db_entity = match[:db_entity]
238
+ entity_id = match[:entity_id] || db_entity.short
239
+ entity_name = db_entity.respond_to?(:names) ? db_entity.names&.first : "unnamed"
240
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:ttl_uri])
241
+
242
+ ttl_label = match[:ttl_entity] ? (match[:ttl_entity][:label] || match[:ttl_entity][:name]) : "Unknown"
243
+
244
+ rows << [
245
+ "UnitsDB: #{entity_id}",
246
+ "(#{entity_name})"
247
+ ]
248
+ rows << [
249
+ "SI TTL: #{si_suffix}",
250
+ "(#{ttl_label})"
251
+ ]
252
+ rows << :separator unless match == matches.last
253
+ end
254
+
255
+ table = Terminal::Table.new(
256
+ title: "Valid SI References",
257
+ rows: rows
258
+ )
259
+ puts table
260
+ end
261
+
262
+ puts "\n=== #{entity_type.capitalize} that should reference SI ==="
263
+ if missing_refs.empty?
264
+ puts "None"
265
+ else
266
+ # Split missing_refs into exact and potential matches
267
+ exact_matches = []
268
+ potential_matches = []
269
+
270
+ missing_refs.each do |match|
271
+ # Determine match type
272
+ ttl_entities = match[:ttl_entities]
273
+ uri = ttl_entities.first[:uri]
274
+ match_type = "Exact match" # Default
275
+ match_type = match[:match_types][uri] if match[:match_types] && match[:match_types][uri]
276
+
277
+ # Get match description if available
278
+ entity_id = match[:db_entity].short
279
+ match_pair_key = "#{entity_id}:#{ttl_entities.first[:uri]}"
280
+ match_details = Unitsdb::Commands::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
281
+ match_desc = match_details[:match_desc] if match_details && match_details[:match_desc]
282
+
283
+ # Symbol matches and partial matches should always be potential matches
284
+ if %w[symbol_match partial_match].include?(match_desc)
285
+ potential_matches << match
286
+ elsif match_type == "Exact match"
287
+ exact_matches << match
288
+ else
289
+ potential_matches << match
290
+ end
291
+ end
292
+
293
+ # Display exact matches
294
+ puts "\n=== Exact Matches (#{exact_matches.size}) ==="
295
+ if exact_matches.empty?
296
+ puts "None"
297
+ else
298
+ rows = []
299
+ exact_matches.each do |match|
300
+ db_entity = match[:db_entity]
301
+ entity_id = match[:entity_id] || db_entity.short
302
+ entity_name = db_entity.respond_to?(:names) ? db_entity.names&.first : "unnamed"
303
+
304
+ # Handle multiple TTL entities in a single row
305
+ ttl_entities = match[:ttl_entities]
306
+ if ttl_entities.size == 1
307
+ # Single TTL entity
308
+ ttl_entity = ttl_entities.first
309
+ si_suffix = SiTtlParser.extract_identifying_suffix(ttl_entity[:uri])
310
+
311
+ rows << [
312
+ "UnitsDB: #{entity_id}",
313
+ "(#{entity_name})"
314
+ ]
315
+ rows << [
316
+ "SI TTL: #{si_suffix}",
317
+ "(#{ttl_entity[:label] || ttl_entity[:name] || "unnamed"})"
318
+ ]
319
+ else
320
+ # Multiple TTL entities, combine them - ensure no duplicates
321
+ si_text_parts = []
322
+ seen_uris = {}
323
+
324
+ ttl_entities.each do |ttl_entity|
325
+ uri = ttl_entity[:uri]
326
+ next if seen_uris[uri] # Skip if we've already seen this URI
327
+
328
+ seen_uris[uri] = true
329
+
330
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
331
+ si_text_parts << "#{suffix} (#{ttl_entity[:label] || ttl_entity[:name] || "unnamed"})"
332
+ end
333
+
334
+ si_text = si_text_parts.join(", ")
335
+
336
+ rows << [
337
+ "UnitsDB: #{entity_id}",
338
+ "(#{entity_name})"
339
+ ]
340
+ rows << [
341
+ "SI TTL: #{si_text}",
342
+ ""
343
+ ]
344
+ end
345
+
346
+ # Get match details for this match
347
+ match_pair_key = "#{db_entity.short}:#{ttl_entities.first[:uri]}"
348
+ match_details = Unitsdb::Commands::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
349
+
350
+ # Format match info
351
+ match_info = ""
352
+ match_info = format_match_info(match_details[:match_desc]) if match_details
353
+
354
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference (#{match_info})"
355
+ rows << [
356
+ "Status: #{status_text}",
357
+ "✗"
358
+ ]
359
+ rows << :separator unless match == exact_matches.last
360
+ end
361
+
362
+ table = Terminal::Table.new(
363
+ title: "Exact Match Missing SI References",
364
+ rows: rows
365
+ )
366
+ puts table
367
+ end
368
+
369
+ # Display potential matches
370
+ puts "\n=== Potential Matches (#{potential_matches.size}) ==="
371
+ if potential_matches.empty?
372
+ puts "None"
373
+ else
374
+ rows = []
375
+ potential_matches.each do |match|
376
+ db_entity = match[:db_entity]
377
+ entity_id = match[:entity_id] || db_entity.short
378
+ entity_name = db_entity.respond_to?(:names) ? db_entity.names&.first : "unnamed"
379
+
380
+ # Handle multiple TTL entities in a single row
381
+ ttl_entities = match[:ttl_entities]
382
+ if ttl_entities.size == 1
383
+ # Single TTL entity
384
+ ttl_entity = ttl_entities.first
385
+ si_suffix = SiTtlParser.extract_identifying_suffix(ttl_entity[:uri])
386
+
387
+ rows << [
388
+ "UnitsDB: #{entity_id}",
389
+ "(#{entity_name})"
390
+ ]
391
+ rows << [
392
+ "SI TTL: #{si_suffix}",
393
+ "(#{ttl_entity[:label] || ttl_entity[:name] || "unnamed"})"
394
+ ]
395
+ else
396
+ # Multiple TTL entities, combine them - ensure no duplicates
397
+ si_text_parts = []
398
+ seen_uris = {}
399
+
400
+ ttl_entities.each do |ttl_entity|
401
+ uri = ttl_entity[:uri]
402
+ next if seen_uris[uri] # Skip if we've already seen this URI
403
+
404
+ seen_uris[uri] = true
405
+
406
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
407
+ si_text_parts << "#{suffix} (#{ttl_entity[:label] || ttl_entity[:name] || "unnamed"})"
408
+ end
409
+
410
+ si_text = si_text_parts.join(", ")
411
+
412
+ rows << [
413
+ "UnitsDB: #{entity_id}",
414
+ "(#{entity_name})"
415
+ ]
416
+ rows << [
417
+ "SI TTL: #{si_text}",
418
+ ""
419
+ ]
420
+ end
421
+
422
+ # Get match details
423
+ match_pair_key = "#{db_entity.short}:#{ttl_entities.first[:uri]}"
424
+ match_details = Unitsdb::Commands::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
425
+
426
+ # Format match info
427
+ match_info = ""
428
+ match_info = format_match_info(match_details[:match_desc]) if match_details
429
+
430
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference (#{match_info})"
431
+ rows << [
432
+ "Status: #{status_text}",
433
+ "✗"
434
+ ]
435
+ rows << :separator unless match == potential_matches.last
436
+ end
437
+
438
+ table = Terminal::Table.new(
439
+ title: "Potential Match Missing SI References",
440
+ rows: rows
441
+ )
442
+ puts table
443
+ end
444
+ end
445
+ end
446
+
447
+ # Print direction header
448
+ def print_direction_header(direction)
449
+ case direction
450
+ when "SI → UnitsDB"
451
+ puts "\n=== Checking SI → UnitsDB (TTL entities referenced by database) ==="
452
+ when "UnitsDB → SI"
453
+ puts "\n=== Checking UnitsDB → SI (database entities referencing TTL) ==="
454
+ end
455
+
456
+ puts "\n=== Instructions for #{direction} direction ==="
457
+ case direction
458
+ when "SI → UnitsDB"
459
+ puts "If you are the UnitsDB Register Manager, please ensure that all SI entities have proper references in the UnitsDB database."
460
+ puts "For each missing reference, add a reference with the appropriate URI and 'authority: \"si-digital-framework\"'."
461
+ when "UnitsDB → SI"
462
+ puts "If you are the UnitsDB Register Manager, please add SI references to UnitsDB entities that should have them."
463
+ puts "For each entity that should reference SI, add a reference with 'authority: \"si-digital-framework\"' and the SI TTL URI."
464
+ end
465
+ end
466
+
467
+ def set_match_details(details)
468
+ @match_details = details
469
+ end
470
+
471
+ # Format match info for display
472
+ def format_match_info(match_desc)
473
+ {
474
+ "short_to_name" => "short → name",
475
+ "short_to_label" => "short → label",
476
+ "name_to_name" => "name → name",
477
+ "name_to_label" => "name → label",
478
+ "name_to_alt_label" => "name → alt_label",
479
+ "symbol_match" => "symbol → symbol",
480
+ "partial_match" => "partial match"
481
+ }[match_desc] || ""
482
+ end
483
+ end
484
+ end
485
+ end