unitsdb 0.1.1 → 2.0.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 (75) 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 +168 -15
  7. data/Gemfile +3 -2
  8. data/README.adoc +803 -1
  9. data/exe/unitsdb +7 -0
  10. data/lib/unitsdb/cli.rb +88 -0
  11. data/lib/unitsdb/commands/_modify.rb +22 -0
  12. data/lib/unitsdb/commands/base.rb +26 -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 +73 -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/ucum/check.rb +126 -0
  23. data/lib/unitsdb/commands/ucum/formatter.rb +141 -0
  24. data/lib/unitsdb/commands/ucum/matcher.rb +301 -0
  25. data/lib/unitsdb/commands/ucum/update.rb +84 -0
  26. data/lib/unitsdb/commands/ucum/updater.rb +98 -0
  27. data/lib/unitsdb/commands/ucum/xml_parser.rb +34 -0
  28. data/lib/unitsdb/commands/ucum.rb +43 -0
  29. data/lib/unitsdb/commands/validate/identifiers.rb +42 -0
  30. data/lib/unitsdb/commands/validate/references.rb +318 -0
  31. data/lib/unitsdb/commands/validate/si_references.rb +109 -0
  32. data/lib/unitsdb/commands/validate.rb +40 -0
  33. data/lib/unitsdb/config.rb +19 -0
  34. data/lib/unitsdb/database.rb +662 -0
  35. data/lib/unitsdb/dimension.rb +19 -25
  36. data/lib/unitsdb/dimension_details.rb +20 -0
  37. data/lib/unitsdb/dimension_reference.rb +8 -0
  38. data/lib/unitsdb/dimensions.rb +4 -6
  39. data/lib/unitsdb/errors.rb +13 -0
  40. data/lib/unitsdb/external_reference.rb +14 -0
  41. data/lib/unitsdb/identifier.rb +8 -0
  42. data/lib/unitsdb/localized_string.rb +17 -0
  43. data/lib/unitsdb/prefix.rb +11 -12
  44. data/lib/unitsdb/prefix_reference.rb +10 -0
  45. data/lib/unitsdb/prefixes.rb +4 -6
  46. data/lib/unitsdb/quantities.rb +4 -27
  47. data/lib/unitsdb/quantity.rb +12 -24
  48. data/lib/unitsdb/quantity_reference.rb +4 -7
  49. data/lib/unitsdb/root_unit_reference.rb +14 -0
  50. data/lib/unitsdb/scale.rb +17 -0
  51. data/lib/unitsdb/scale_properties.rb +12 -0
  52. data/lib/unitsdb/scale_reference.rb +10 -0
  53. data/lib/unitsdb/scales.rb +12 -0
  54. data/lib/unitsdb/si_derived_base.rb +13 -14
  55. data/lib/unitsdb/symbol_presentations.rb +14 -0
  56. data/lib/unitsdb/ucum.rb +198 -0
  57. data/lib/unitsdb/unit.rb +20 -26
  58. data/lib/unitsdb/unit_reference.rb +5 -8
  59. data/lib/unitsdb/unit_system.rb +8 -10
  60. data/lib/unitsdb/unit_system_reference.rb +10 -0
  61. data/lib/unitsdb/unit_systems.rb +4 -16
  62. data/lib/unitsdb/units.rb +4 -6
  63. data/lib/unitsdb/utils.rb +84 -0
  64. data/lib/unitsdb/version.rb +1 -1
  65. data/lib/unitsdb.rb +13 -10
  66. data/unitsdb.gemspec +6 -3
  67. metadata +120 -12
  68. data/lib/unitsdb/dimension_quantity.rb +0 -28
  69. data/lib/unitsdb/dimension_symbol.rb +0 -22
  70. data/lib/unitsdb/prefix_symbol.rb +0 -12
  71. data/lib/unitsdb/root_unit.rb +0 -17
  72. data/lib/unitsdb/root_units.rb +0 -20
  73. data/lib/unitsdb/symbol.rb +0 -17
  74. data/lib/unitsdb/unit_symbol.rb +0 -15
  75. data/lib/unitsdb/unitsdb.rb +0 -6
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ module Validate
8
+ class References < Unitsdb::Commands::Base
9
+ def run
10
+ # Load the database
11
+ db = load_database(@options[:database])
12
+
13
+ # Build registry of all valid IDs
14
+ registry = build_id_registry(db)
15
+
16
+ # Check all references
17
+ invalid_refs = check_references(db, registry)
18
+
19
+ # Display results
20
+ display_reference_results(invalid_refs, registry)
21
+ rescue Unitsdb::Errors::DatabaseError => e
22
+ puts "Error: #{e.message}"
23
+ exit(1)
24
+ end
25
+
26
+ private
27
+
28
+ def build_id_registry(db)
29
+ registry = {}
30
+
31
+ # Add all unit identifiers to the registry
32
+ registry["units"] = {}
33
+ db.units.each_with_index do |unit, index|
34
+ unit.identifiers.each do |identifier|
35
+ next unless identifier.id && identifier.type
36
+
37
+ # Add the composite key (type:id)
38
+ composite_key = "#{identifier.type}:#{identifier.id}"
39
+ registry["units"][composite_key] = "index:#{index}"
40
+
41
+ # Also add just the ID for backward compatibility
42
+ registry["units"][identifier.id] = "index:#{index}"
43
+ end
44
+ end
45
+
46
+ # Add dimension identifiers
47
+ registry["dimensions"] = {}
48
+ db.dimensions.each_with_index do |dimension, index|
49
+ dimension.identifiers.each do |identifier|
50
+ next unless identifier.id && identifier.type
51
+
52
+ composite_key = "#{identifier.type}:#{identifier.id}"
53
+ registry["dimensions"][composite_key] = "index:#{index}"
54
+ registry["dimensions"][identifier.id] = "index:#{index}"
55
+ end
56
+
57
+ # Also track dimensions by short name
58
+ if dimension.respond_to?(:short) && dimension.short
59
+ registry["dimensions_short"] ||= {}
60
+ registry["dimensions_short"][dimension.short] = "index:#{index}"
61
+ end
62
+ end
63
+
64
+ # Add quantity identifiers
65
+ registry["quantities"] = {}
66
+ db.quantities.each_with_index do |quantity, index|
67
+ quantity.identifiers.each do |identifier|
68
+ next unless identifier.id && identifier.type
69
+
70
+ composite_key = "#{identifier.type}:#{identifier.id}"
71
+ registry["quantities"][composite_key] = "index:#{index}"
72
+ registry["quantities"][identifier.id] = "index:#{index}"
73
+ end
74
+ end
75
+
76
+ # Add prefix identifiers
77
+ registry["prefixes"] = {}
78
+ db.prefixes.each_with_index do |prefix, index|
79
+ prefix.identifiers.each do |identifier|
80
+ next unless identifier.id && identifier.type
81
+
82
+ composite_key = "#{identifier.type}:#{identifier.id}"
83
+ registry["prefixes"][composite_key] = "index:#{index}"
84
+ registry["prefixes"][identifier.id] = "index:#{index}"
85
+ end
86
+ end
87
+
88
+ # Add unit system identifiers
89
+ registry["unit_systems"] = {}
90
+ db.unit_systems.each_with_index do |unit_system, index|
91
+ unit_system.identifiers.each do |identifier|
92
+ next unless identifier.id && identifier.type
93
+
94
+ composite_key = "#{identifier.type}:#{identifier.id}"
95
+ registry["unit_systems"][composite_key] = "index:#{index}"
96
+ registry["unit_systems"][identifier.id] = "index:#{index}"
97
+ end
98
+
99
+ # Also track unit systems by short name
100
+ if unit_system.respond_to?(:short) && unit_system.short
101
+ registry["unit_systems_short"] ||= {}
102
+ registry["unit_systems_short"][unit_system.short] = "index:#{index}"
103
+ end
104
+ end
105
+
106
+ # Debug registry if requested
107
+ if @options[:debug_registry]
108
+ puts "Registry contents:"
109
+ registry.each do |type, ids|
110
+ puts " #{type}:"
111
+ ids.each do |id, location|
112
+ puts " #{id} => #{location}"
113
+ end
114
+ end
115
+ end
116
+
117
+ registry
118
+ end
119
+
120
+ def check_references(db, registry)
121
+ invalid_refs = {}
122
+
123
+ # Check unit references in dimensions
124
+ check_dimension_references(db, registry, invalid_refs)
125
+
126
+ # Check unit_system references
127
+ check_unit_system_references(db, registry, invalid_refs)
128
+
129
+ # Check quantity references
130
+ check_quantity_references(db, registry, invalid_refs)
131
+
132
+ # Check root unit references in units
133
+ check_root_unit_references(db, registry, invalid_refs)
134
+
135
+ invalid_refs
136
+ end
137
+
138
+ def check_dimension_references(db, registry, invalid_refs)
139
+ db.dimensions.each_with_index do |dimension, index|
140
+ next unless dimension.respond_to?(:dimension_reference) && dimension.dimension_reference
141
+
142
+ ref_id = dimension.dimension_reference
143
+ ref_type = "dimensions"
144
+ ref_path = "dimensions:index:#{index}:dimension_reference"
145
+
146
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "dimensions")
147
+ end
148
+ end
149
+
150
+ def check_unit_system_references(db, registry, invalid_refs)
151
+ db.units.each_with_index do |unit, index|
152
+ next unless unit.respond_to?(:unit_system_reference) && unit.unit_system_reference
153
+
154
+ unit.unit_system_reference.each_with_index do |ref_id, idx|
155
+ ref_type = "unit_systems"
156
+ ref_path = "units:index:#{index}:unit_system_reference[#{idx}]"
157
+
158
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
159
+ end
160
+ end
161
+ end
162
+
163
+ def check_quantity_references(db, registry, invalid_refs)
164
+ db.units.each_with_index do |unit, index|
165
+ next unless unit.respond_to?(:quantity_references) && unit.quantity_references
166
+
167
+ unit.quantity_references.each_with_index do |ref_id, idx|
168
+ ref_type = "quantities"
169
+ ref_path = "units:index:#{index}:quantity_references[#{idx}]"
170
+
171
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
172
+ end
173
+ end
174
+ end
175
+
176
+ def check_root_unit_references(db, registry, invalid_refs)
177
+ db.units.each_with_index do |unit, index|
178
+ next unless unit.respond_to?(:root_units) && unit.root_units
179
+
180
+ unit.root_units.each_with_index do |root_unit, idx|
181
+ next unless root_unit.respond_to?(:unit_reference) && root_unit.unit_reference
182
+
183
+ # Check unit reference
184
+ ref_id = root_unit.unit_reference
185
+ ref_type = "units"
186
+ ref_path = "units:index:#{index}:root_units.#{idx}.unit_reference"
187
+
188
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
189
+
190
+ # Check prefix reference if present
191
+ next unless root_unit.respond_to?(:prefix_reference) && root_unit.prefix_reference
192
+
193
+ ref_id = root_unit.prefix_reference
194
+ ref_type = "prefixes"
195
+ ref_path = "units:index:#{index}:root_units.#{idx}.prefix_reference"
196
+
197
+ validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, "units")
198
+ end
199
+ end
200
+ end
201
+
202
+ def validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs, file_type)
203
+ # Handle references that are objects with id and type (could be a hash or an object)
204
+ if ref_id.respond_to?(:id) && ref_id.respond_to?(:type)
205
+ id = ref_id.id
206
+ type = ref_id.type
207
+ composite_key = "#{type}:#{id}"
208
+
209
+ # Try multiple lookup strategies
210
+ valid = false
211
+
212
+ # 1. Try exact composite key match
213
+ valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
214
+
215
+ # 2. Try just ID match if composite didn't work
216
+ valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
217
+
218
+ # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
219
+ if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
220
+ registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
221
+ registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
222
+ registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
223
+ )
224
+ # Special handling for unit_systems between unitsml and nist types
225
+ valid = true
226
+ end
227
+
228
+ if valid
229
+ puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
230
+ else
231
+ invalid_refs[file_type] ||= {}
232
+ invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
233
+ end
234
+ # Handle references that are objects with id and type in a hash
235
+ elsif ref_id.is_a?(Hash) && ref_id.key?("id") && ref_id.key?("type")
236
+ id = ref_id["id"]
237
+ type = ref_id["type"]
238
+ composite_key = "#{type}:#{id}"
239
+
240
+ # Try multiple lookup strategies
241
+ valid = false
242
+
243
+ # 1. Try exact composite key match
244
+ valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
245
+
246
+ # 2. Try just ID match if composite didn't work
247
+ valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
248
+
249
+ # 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
250
+ if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
251
+ registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
252
+ registry[ref_type].keys.any? { |k| k.end_with?(":SI_#{id.sub("si-", "")}") } ||
253
+ registry[ref_type].keys.any? { |k| k.end_with?(":non-SI_#{id.sub("nonsi-", "")}") }
254
+ )
255
+ # Special handling for unit_systems between unitsml and nist types
256
+ valid = true
257
+ end
258
+
259
+ if valid
260
+ puts "Valid reference: #{id} (#{type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
261
+ else
262
+ invalid_refs[file_type] ||= {}
263
+ invalid_refs[file_type][ref_path] = { id: id, type: type, ref_type: ref_type }
264
+ end
265
+ else
266
+ # Handle plain string references (legacy format)
267
+ valid = registry.key?(ref_type) && registry[ref_type].key?(ref_id)
268
+
269
+ if valid
270
+ puts "Valid reference: #{ref_id} (#{ref_type}) at #{file_type}:#{ref_path}" if @options[:print_valid]
271
+ else
272
+ invalid_refs[file_type] ||= {}
273
+ invalid_refs[file_type][ref_path] = { id: ref_id, type: ref_type }
274
+ end
275
+ end
276
+ end
277
+
278
+ def display_reference_results(invalid_refs, registry)
279
+ if invalid_refs.empty?
280
+ puts "All references are valid!"
281
+ return
282
+ end
283
+
284
+ puts "Found invalid references:"
285
+
286
+ # Display registry contents if debug_registry is enabled
287
+ # This is needed for the failing test
288
+ if @options[:debug_registry]
289
+ puts "\nRegistry contents:"
290
+ registry.each do |type, ids|
291
+ next if ids.empty?
292
+
293
+ puts " #{type}:"
294
+ ids.each do |id, location|
295
+ puts " #{id}: {type: #{type.sub("s", "")}, source: #{location}}"
296
+ end
297
+ end
298
+ end
299
+ invalid_refs.each do |file, refs|
300
+ puts " #{file}:"
301
+ refs.each do |path, ref|
302
+ puts " #{path} => '#{ref[:id]}' (#{ref[:type]})"
303
+
304
+ # Suggest corrections
305
+ next unless registry.key?(ref[:ref_type])
306
+
307
+ similar_ids = Unitsdb::Utils.find_similar_ids(ref[:id], registry[ref[:ref_type]].keys)
308
+ if similar_ids.any?
309
+ puts " Did you mean one of these?"
310
+ similar_ids.each { |id| puts " - #{id}" }
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ module Validate
8
+ class SiReferences < Unitsdb::Commands::Base
9
+ def run
10
+ # Load the database
11
+ db = load_database(@options[:database])
12
+
13
+ # Check for duplicate SI references
14
+ duplicates = check_si_references(db)
15
+
16
+ # Display results
17
+ display_duplicate_results(duplicates)
18
+ rescue Unitsdb::Errors::DatabaseError => e
19
+ puts "Error: #{e.message}"
20
+ exit(1)
21
+ end
22
+
23
+ private
24
+
25
+ def check_si_references(db)
26
+ duplicates = {}
27
+
28
+ # Check units
29
+ check_entity_si_references(db.units, "units", duplicates)
30
+
31
+ # Check quantities
32
+ check_entity_si_references(db.quantities, "quantities", duplicates)
33
+
34
+ # Check prefixes
35
+ check_entity_si_references(db.prefixes, "prefixes", duplicates)
36
+
37
+ duplicates
38
+ end
39
+
40
+ def check_entity_si_references(entities, entity_type, duplicates)
41
+ # Track SI references by URI
42
+ si_refs = {}
43
+
44
+ entities.each_with_index do |entity, index|
45
+ # Skip if no references
46
+ next unless entity.respond_to?(:references) && entity.references
47
+
48
+ # Check each reference
49
+ entity.references.each do |ref|
50
+ # Only interested in si-digital-framework references
51
+ next unless ref.authority == "si-digital-framework"
52
+
53
+ # Get entity info for display
54
+ entity_id = if entity.respond_to?(:identifiers) && entity.identifiers&.first.respond_to?(:id)
55
+ entity.identifiers.first.id
56
+ else
57
+ entity.short
58
+ end
59
+
60
+ # Track this reference
61
+ si_refs[ref.uri] ||= []
62
+ si_refs[ref.uri] << {
63
+ entity_id: entity_id,
64
+ entity_name: entity.respond_to?(:names) ? entity.names.first : entity.short,
65
+ index: index
66
+ }
67
+ end
68
+ end
69
+
70
+ # Find duplicates (URIs with more than one entity)
71
+ si_refs.each do |uri, entities|
72
+ next unless entities.size > 1
73
+
74
+ # Record this duplicate
75
+ duplicates[entity_type] ||= {}
76
+ duplicates[entity_type][uri] = entities
77
+ end
78
+ end
79
+
80
+ def display_duplicate_results(duplicates)
81
+ if duplicates.empty?
82
+ puts "No duplicate SI references found! Each SI reference URI is used by at most one entity of each type."
83
+ return
84
+ end
85
+
86
+ puts "Found duplicate SI references:"
87
+
88
+ duplicates.each do |entity_type, uri_duplicates|
89
+ puts "\n #{entity_type.capitalize}:"
90
+
91
+ uri_duplicates.each do |uri, entities|
92
+ puts " SI URI: #{uri}"
93
+ puts " Used by #{entities.size} entities:"
94
+
95
+ entities.each do |entity|
96
+ puts " - #{entity[:entity_id]} (#{entity[:entity_name]}) at index #{entity[:index]}"
97
+ end
98
+ puts ""
99
+ end
100
+ end
101
+
102
+ puts "\nEach SI digital framework reference should be used by at most one entity of each type."
103
+ puts "Please fix the duplicates by either removing the reference from all but one entity,"
104
+ puts "or by updating the references to use different URIs appropriate for each entity."
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ class ValidateCommand < Thor
8
+ desc "references", "Validate that all references exist"
9
+ option :debug_registry, type: :boolean, desc: "Show registry contents for debugging"
10
+ option :database, type: :string, required: true, aliases: "-d",
11
+ desc: "Path to UnitsDB database (required)"
12
+ option :print_valid, type: :boolean, default: false, desc: "Print valid references too"
13
+ def references
14
+ require_relative "validate/references"
15
+
16
+ Commands::Validate::References.new(options).run
17
+ end
18
+
19
+ desc "identifiers", "Check for uniqueness of identifier fields"
20
+ option :database, type: :string, required: true, aliases: "-d",
21
+ desc: "Path to UnitsDB database (required)"
22
+
23
+ def identifiers
24
+ require_relative "validate/identifiers"
25
+
26
+ Commands::Validate::Identifiers.new(options).run
27
+ end
28
+
29
+ desc "si_references", "Validate that each SI digital framework reference is unique per entity type"
30
+ option :database, type: :string, required: true, aliases: "-d",
31
+ desc: "Path to UnitsDB database (required)"
32
+
33
+ def si_references
34
+ require_relative "validate/si_references"
35
+
36
+ Commands::Validate::SiReferences.new(options).run
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ class Config
5
+ class << self
6
+ def models
7
+ @models ||= {}
8
+ end
9
+
10
+ def models=(user_models)
11
+ models.merge!(user_models)
12
+ end
13
+
14
+ def model_for(model_name)
15
+ models[model_name]
16
+ end
17
+ end
18
+ end
19
+ end