unitsdb 2.2.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/opal.yml +36 -0
- data/.gitignore +3 -0
- data/Gemfile +1 -0
- data/Rakefile +3 -1
- data/lib/unitsdb/cli.rb +5 -41
- data/lib/unitsdb/commands/_modify.rb +1 -34
- data/lib/unitsdb/commands/check_si/si_formatter.rb +6 -6
- data/lib/unitsdb/commands/check_si/si_matcher.rb +202 -292
- data/lib/unitsdb/commands/check_si/si_updater.rb +16 -36
- data/lib/unitsdb/commands/entity_presenter.rb +98 -0
- data/lib/unitsdb/commands/get.rb +16 -113
- data/lib/unitsdb/commands/qudt/formatter.rb +16 -27
- data/lib/unitsdb/commands/qudt/matcher.rb +18 -28
- data/lib/unitsdb/commands/qudt/updater.rb +8 -11
- data/lib/unitsdb/commands/qudt.rb +1 -34
- data/lib/unitsdb/commands/search.rb +33 -188
- data/lib/unitsdb/commands/thor.rb +41 -0
- data/lib/unitsdb/commands/ucum/formatter.rb +9 -18
- data/lib/unitsdb/commands/ucum/matcher.rb +4 -4
- data/lib/unitsdb/commands/ucum/updater.rb +3 -5
- data/lib/unitsdb/commands/ucum.rb +1 -34
- data/lib/unitsdb/commands/validate/qudt_references.rb +29 -70
- data/lib/unitsdb/commands/validate/references.rb +5 -303
- data/lib/unitsdb/commands/validate/si_references.rb +30 -66
- data/lib/unitsdb/commands/validate/ucum_references.rb +30 -64
- data/lib/unitsdb/commands/validate.rb +1 -36
- data/lib/unitsdb/commands.rb +2 -0
- data/lib/unitsdb/config.rb +170 -4
- data/lib/unitsdb/database/loader.rb +135 -0
- data/lib/unitsdb/database/reference_validator.rb +227 -0
- data/lib/unitsdb/database/uniqueness_validator.rb +80 -0
- data/lib/unitsdb/database.rb +127 -588
- data/lib/unitsdb/dimension.rb +2 -27
- data/lib/unitsdb/dimension_details.rb +2 -0
- data/lib/unitsdb/dimension_reference.rb +2 -0
- data/lib/unitsdb/dimensions.rb +2 -2
- data/lib/unitsdb/external_reference.rb +2 -0
- data/lib/unitsdb/identifier.rb +2 -0
- data/lib/unitsdb/localized_string.rb +2 -0
- data/lib/unitsdb/opal.rb +43 -0
- data/lib/unitsdb/prefix.rb +2 -13
- data/lib/unitsdb/prefix_reference.rb +2 -0
- data/lib/unitsdb/prefixes.rb +2 -2
- data/lib/unitsdb/quantities.rb +2 -1
- data/lib/unitsdb/quantity.rb +2 -2
- data/lib/unitsdb/quantity_reference.rb +2 -2
- data/lib/unitsdb/qudt.rb +5 -0
- data/lib/unitsdb/root_unit_reference.rb +2 -2
- data/lib/unitsdb/scale.rb +2 -2
- data/lib/unitsdb/scale_properties.rb +2 -1
- data/lib/unitsdb/scale_reference.rb +2 -0
- data/lib/unitsdb/scales.rb +2 -1
- data/lib/unitsdb/si_derived_base.rb +2 -1
- data/lib/unitsdb/symbol_presentations.rb +2 -2
- data/lib/unitsdb/ucum.rb +7 -0
- data/lib/unitsdb/unit.rb +2 -34
- data/lib/unitsdb/unit_reference.rb +2 -0
- data/lib/unitsdb/unit_system.rb +2 -2
- data/lib/unitsdb/unit_system_reference.rb +2 -0
- data/lib/unitsdb/unit_systems.rb +2 -2
- data/lib/unitsdb/units.rb +2 -2
- data/lib/unitsdb/version.rb +1 -1
- data/lib/unitsdb.rb +134 -27
- data/unitsdb.gemspec +1 -0
- metadata +23 -2
data/lib/unitsdb/database.rb
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Unitsdb
|
|
4
4
|
class Database < Lutaml::Model::Serializable
|
|
5
|
-
|
|
5
|
+
autoload :ReferenceValidator, "unitsdb/database/reference_validator"
|
|
6
|
+
autoload :UniquenessValidator, "unitsdb/database/uniqueness_validator"
|
|
7
|
+
autoload :Loader, "unitsdb/database/loader"
|
|
8
|
+
|
|
9
|
+
COLLECTIONS = Loader::DATABASE_FILES.keys.map(&:to_sym).freeze
|
|
10
|
+
|
|
11
|
+
# Collections whose entities carry a `symbols` attribute.
|
|
12
|
+
# Used by symbol-based search/match to narrow iteration.
|
|
13
|
+
SYMBOL_COLLECTIONS = %i[units prefixes].freeze
|
|
6
14
|
|
|
7
15
|
attribute :schema_version, :string
|
|
8
16
|
attribute :version, :string
|
|
@@ -12,16 +20,47 @@ module Unitsdb
|
|
|
12
20
|
attribute :dimensions, Dimension, collection: true
|
|
13
21
|
attribute :unit_systems, UnitSystem, collection: true
|
|
14
22
|
|
|
23
|
+
# Resolve a collection name (String or Symbol) to its typed Array.
|
|
24
|
+
# Validates against COLLECTIONS so caller-supplied names can never
|
|
25
|
+
# dispatch to arbitrary methods (e.g. `schema_version`, `to_yaml`).
|
|
26
|
+
def collection(name)
|
|
27
|
+
sym = name.to_sym
|
|
28
|
+
unless COLLECTIONS.include?(sym)
|
|
29
|
+
raise ArgumentError, "unknown collection: #{name.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
public_send(sym)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Build an empty Database with `entities` partitioned into their
|
|
36
|
+
# typed collections. Used by CLI commands that need to serialize
|
|
37
|
+
# a subset of search results.
|
|
38
|
+
def self.empty_for_results(entities)
|
|
39
|
+
Database.new.tap do |db|
|
|
40
|
+
COLLECTIONS.each do |name|
|
|
41
|
+
klass = collection_element_class(name)
|
|
42
|
+
db.public_send("#{name}=", entities.grep(klass))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Map a collection symbol to the entity class it holds.
|
|
51
|
+
# Derived from the Lutaml attribute declaration.
|
|
52
|
+
def collection_element_class(name)
|
|
53
|
+
attributes.fetch(name.to_s).type
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
15
57
|
# Find an entity by its specific identifier and type
|
|
16
58
|
# @param id [String] the identifier value to search for
|
|
17
59
|
# @param type [String, Symbol] the entity type (units, prefixes, quantities, etc.)
|
|
18
60
|
# @return [Object, nil] the first entity with matching identifier or nil if not found
|
|
19
61
|
def find_by_type(id:, type:)
|
|
20
|
-
collection
|
|
21
|
-
|
|
22
|
-
entity.identifiers&.any? do |identifier|
|
|
23
|
-
identifier.id == id
|
|
24
|
-
end
|
|
62
|
+
collection(type).find do |entity|
|
|
63
|
+
entity.identifiers.any? { |identifier| identifier.id == id }
|
|
25
64
|
end
|
|
26
65
|
end
|
|
27
66
|
|
|
@@ -30,82 +69,36 @@ module Unitsdb
|
|
|
30
69
|
# @param type [String, nil] optional identifier type to match
|
|
31
70
|
# @return [Object, nil] the first entity with matching identifier or nil if not found
|
|
32
71
|
def get_by_id(id:, type: nil)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
collection = send(collection_name)
|
|
38
|
-
entity = collection.find do |e|
|
|
39
|
-
e.identifiers&.any? do |identifier|
|
|
72
|
+
COLLECTIONS.each do |name|
|
|
73
|
+
entity = collection(name).find do |e|
|
|
74
|
+
e.identifiers.any? do |identifier|
|
|
40
75
|
identifier.id == id && (type.nil? || identifier.type == type)
|
|
41
76
|
end
|
|
42
77
|
end
|
|
43
|
-
|
|
44
78
|
return entity if entity
|
|
45
79
|
end
|
|
46
80
|
|
|
47
81
|
nil
|
|
48
82
|
end
|
|
49
83
|
|
|
50
|
-
# Search for entities containing the given text in identifiers,
|
|
84
|
+
# Search for entities containing the given text in identifiers,
|
|
85
|
+
# names, or short description.
|
|
51
86
|
# @param params [Hash] search parameters
|
|
52
87
|
# @option params [String] :text The text to search for
|
|
53
88
|
# @option params [String, Symbol, nil] :type Optional entity type to limit search scope
|
|
54
89
|
# @return [Array] all entities matching the search criteria
|
|
55
90
|
def search(params = {})
|
|
56
91
|
text = params[:text]
|
|
57
|
-
type = params[:type]
|
|
58
|
-
|
|
59
92
|
return [] unless text
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
needle = text.downcase
|
|
95
|
+
scope = scope_for(params[:type], COLLECTIONS)
|
|
62
96
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else
|
|
67
|
-
%w[units prefixes quantities
|
|
68
|
-
dimensions unit_systems]
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
collections.each do |collection_name|
|
|
72
|
-
next unless respond_to?(collection_name)
|
|
73
|
-
|
|
74
|
-
collection = send(collection_name)
|
|
75
|
-
collection.each do |entity|
|
|
76
|
-
# Search in identifiers
|
|
77
|
-
if entity.identifiers&.any? do |identifier|
|
|
78
|
-
identifier.id.to_s.downcase.include?(text.downcase)
|
|
79
|
-
end
|
|
80
|
-
results << entity
|
|
81
|
-
next
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Search in names (if the entity has names)
|
|
85
|
-
if entity.respond_to?(:names) && entity.names && entity.names.any? do |name|
|
|
86
|
-
name.value.to_s.downcase.include?(text.downcase)
|
|
87
|
-
end
|
|
88
|
-
results << entity
|
|
89
|
-
next
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Search in short description
|
|
93
|
-
if entity.respond_to?(:short) && entity.short &&
|
|
94
|
-
entity.short.to_s.downcase.include?(text.downcase)
|
|
95
|
-
results << entity
|
|
96
|
-
next
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Special case for prefix name (prefixes don't have names array)
|
|
100
|
-
next unless collection_name == "prefixes" && entity.respond_to?(:name) &&
|
|
101
|
-
entity.name.to_s.downcase.include?(text.downcase)
|
|
102
|
-
|
|
103
|
-
results << entity
|
|
104
|
-
next
|
|
97
|
+
scope.each_with_object([]) do |name, results|
|
|
98
|
+
collection(name).each do |entity|
|
|
99
|
+
results << entity if matches_text?(entity, needle)
|
|
105
100
|
end
|
|
106
101
|
end
|
|
107
|
-
|
|
108
|
-
results
|
|
109
102
|
end
|
|
110
103
|
|
|
111
104
|
# Find entities by symbol
|
|
@@ -115,38 +108,16 @@ module Unitsdb
|
|
|
115
108
|
def find_by_symbol(symbol, entity_type = nil)
|
|
116
109
|
return [] unless symbol
|
|
117
110
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
prefixes].include?(collection_name)
|
|
126
|
-
|
|
127
|
-
collection = send(collection_name)
|
|
128
|
-
collection.each do |entity|
|
|
129
|
-
if collection_name == "units" && entity.respond_to?(:symbols) && entity.symbols
|
|
130
|
-
# Units can have multiple symbols
|
|
131
|
-
matches = entity.symbols.any? do |sym|
|
|
132
|
-
sym.respond_to?(:ascii) && sym.ascii &&
|
|
133
|
-
sym.ascii.downcase == symbol.downcase
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
results << entity if matches
|
|
137
|
-
elsif collection_name == "prefixes" && entity.respond_to?(:symbols) && entity.symbols
|
|
138
|
-
# Prefixes have multiple symbols in 2.0.0
|
|
139
|
-
matches = entity.symbols.any? do |sym|
|
|
140
|
-
sym.respond_to?(:ascii) && sym.ascii &&
|
|
141
|
-
sym.ascii.downcase == symbol.downcase
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
results << entity if matches
|
|
111
|
+
needle = symbol.downcase
|
|
112
|
+
scope = scope_for(entity_type, SYMBOL_COLLECTIONS)
|
|
113
|
+
|
|
114
|
+
scope.each_with_object([]) do |name, results|
|
|
115
|
+
collection(name).each do |entity|
|
|
116
|
+
results << entity if entity.symbols.any? do |sym|
|
|
117
|
+
sym.ascii.to_s.downcase == needle
|
|
145
118
|
end
|
|
146
119
|
end
|
|
147
120
|
end
|
|
148
|
-
|
|
149
|
-
results
|
|
150
121
|
end
|
|
151
122
|
|
|
152
123
|
# Match entities by name, short, or symbol with different match types
|
|
@@ -157,544 +128,112 @@ module Unitsdb
|
|
|
157
128
|
# @return [Hash] matches grouped by match type (exact, symbol_match) with match details
|
|
158
129
|
def match_entities(params = {})
|
|
159
130
|
value = params[:value]
|
|
160
|
-
match_type = params[:match_type]&.to_s || "exact"
|
|
161
|
-
entity_type = params[:entity_type]
|
|
162
|
-
|
|
163
131
|
return {} unless value
|
|
164
132
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
symbol_match: [],
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
# Define collections to search based on entity_type parameter
|
|
171
|
-
collections = if entity_type
|
|
172
|
-
[entity_type.to_s]
|
|
173
|
-
else
|
|
174
|
-
%w[units prefixes
|
|
175
|
-
quantities dimensions unit_systems]
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
collections.each do |collection_name|
|
|
179
|
-
next unless respond_to?(collection_name)
|
|
180
|
-
|
|
181
|
-
collection = send(collection_name)
|
|
133
|
+
match_type = params[:match_type]&.to_s || "exact"
|
|
134
|
+
result = { exact: [], symbol_match: [] }
|
|
182
135
|
|
|
183
|
-
|
|
184
|
-
|
|
136
|
+
scope_for(params[:entity_type], COLLECTIONS).each do |name|
|
|
137
|
+
collection(name).each do |entity|
|
|
185
138
|
if %w[exact all].include?(match_type)
|
|
186
|
-
|
|
187
|
-
if entity.respond_to?(:short) && entity.short &&
|
|
188
|
-
entity.short.downcase == value.downcase
|
|
189
|
-
result[:exact] << {
|
|
190
|
-
entity: entity,
|
|
191
|
-
match_desc: "short_to_name",
|
|
192
|
-
details: "UnitsDB short '#{entity.short}' matches '#{value}'",
|
|
193
|
-
}
|
|
194
|
-
next
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Match by names
|
|
198
|
-
if entity.respond_to?(:names) && entity.names
|
|
199
|
-
matching_name = entity.names.find do |name|
|
|
200
|
-
name.value.to_s.downcase == value.downcase
|
|
201
|
-
end
|
|
202
|
-
if matching_name
|
|
203
|
-
result[:exact] << {
|
|
204
|
-
entity: entity,
|
|
205
|
-
match_desc: "name_to_name",
|
|
206
|
-
details: "UnitsDB name '#{matching_name.value}' (#{matching_name.lang}) matches '#{value}'",
|
|
207
|
-
}
|
|
208
|
-
next
|
|
209
|
-
end
|
|
210
|
-
end
|
|
139
|
+
match_exact(entity, value, result)
|
|
211
140
|
end
|
|
141
|
+
next unless %w[symbol all].include?(match_type) && SYMBOL_COLLECTIONS.include?(name)
|
|
212
142
|
|
|
213
|
-
|
|
214
|
-
if %w[symbol all].include?(match_type) &&
|
|
215
|
-
%w[units prefixes].include?(collection_name)
|
|
216
|
-
if collection_name == "units" && entity.respond_to?(:symbols) && entity.symbols
|
|
217
|
-
# Units can have multiple symbols
|
|
218
|
-
matching_symbol = entity.symbols.find do |sym|
|
|
219
|
-
sym.respond_to?(:ascii) && sym.ascii &&
|
|
220
|
-
sym.ascii.downcase == value.downcase
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
if matching_symbol
|
|
224
|
-
result[:symbol_match] << {
|
|
225
|
-
entity: entity,
|
|
226
|
-
match_desc: "symbol_match",
|
|
227
|
-
details: "UnitsDB symbol '#{matching_symbol.ascii}' matches '#{value}'",
|
|
228
|
-
}
|
|
229
|
-
end
|
|
230
|
-
elsif collection_name == "prefixes" && entity.respond_to?(:symbols) && entity.symbols
|
|
231
|
-
# Prefixes have multiple symbols in 2.0.0
|
|
232
|
-
matching_symbol = entity.symbols.find do |sym|
|
|
233
|
-
sym.respond_to?(:ascii) && sym.ascii &&
|
|
234
|
-
sym.ascii.downcase == value.downcase
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
if matching_symbol
|
|
238
|
-
result[:symbol_match] << {
|
|
239
|
-
entity: entity,
|
|
240
|
-
match_desc: "symbol_match",
|
|
241
|
-
details: "UnitsDB symbol '#{matching_symbol.ascii}' matches '#{value}'",
|
|
242
|
-
}
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
end
|
|
143
|
+
match_symbol(entity, value, result)
|
|
246
144
|
end
|
|
247
145
|
end
|
|
248
146
|
|
|
249
|
-
# Remove empty categories
|
|
250
147
|
result.delete_if { |_, v| v.empty? }
|
|
251
|
-
|
|
252
148
|
result
|
|
253
149
|
end
|
|
254
150
|
|
|
255
|
-
# Checks for uniqueness of identifiers and short names
|
|
151
|
+
# Checks for uniqueness of identifiers and short names.
|
|
152
|
+
# Delegates to UniquenessValidator.
|
|
256
153
|
def validate_uniqueness
|
|
257
|
-
|
|
258
|
-
short: {},
|
|
259
|
-
id: {},
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
# Validate short names for applicable collections
|
|
263
|
-
validate_shorts(units, "units", results)
|
|
264
|
-
validate_shorts(dimensions, "dimensions", results)
|
|
265
|
-
validate_shorts(unit_systems, "unit_systems", results)
|
|
266
|
-
|
|
267
|
-
# Validate identifiers for all collections
|
|
268
|
-
validate_identifiers(units, "units", results)
|
|
269
|
-
validate_identifiers(prefixes, "prefixes", results)
|
|
270
|
-
validate_identifiers(quantities, "quantities", results)
|
|
271
|
-
validate_identifiers(dimensions, "dimensions", results)
|
|
272
|
-
validate_identifiers(unit_systems, "unit_systems", results)
|
|
273
|
-
|
|
274
|
-
results
|
|
154
|
+
UniquenessValidator.validate(self)
|
|
275
155
|
end
|
|
276
156
|
|
|
277
|
-
# Validates references between entities
|
|
157
|
+
# Validates references between entities. Delegates to ReferenceValidator.
|
|
278
158
|
def validate_references
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
# Build registry of all valid IDs first
|
|
282
|
-
registry = build_id_registry
|
|
283
|
-
|
|
284
|
-
# Check various reference types
|
|
285
|
-
check_dimension_references(registry, invalid_refs)
|
|
286
|
-
check_unit_system_references(registry, invalid_refs)
|
|
287
|
-
check_quantity_references(registry, invalid_refs)
|
|
288
|
-
check_root_unit_references(registry, invalid_refs)
|
|
289
|
-
|
|
290
|
-
invalid_refs
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def self.from_db(dir_path)
|
|
294
|
-
# If dir_path is a relative path, make it relative to the current working directory
|
|
295
|
-
db_path = dir_path
|
|
296
|
-
puts "Database directory path: #{db_path}"
|
|
297
|
-
|
|
298
|
-
# Check if the directory exists
|
|
299
|
-
unless Dir.exist?(db_path)
|
|
300
|
-
raise Errors::DatabaseNotFoundError,
|
|
301
|
-
"Database directory not found: #{db_path}"
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
# Define required files
|
|
305
|
-
required_files = %w[prefixes.yaml dimensions.yaml units.yaml
|
|
306
|
-
quantities.yaml unit_systems.yaml]
|
|
307
|
-
yaml_files = required_files.map { |file| File.join(dir_path, file) }
|
|
308
|
-
|
|
309
|
-
# Check if all required files exist
|
|
310
|
-
missing_files = required_files.reject do |file|
|
|
311
|
-
File.exist?(File.join(dir_path, file))
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
if missing_files.any?
|
|
315
|
-
raise Errors::DatabaseFileNotFoundError,
|
|
316
|
-
"Missing required database files: #{missing_files.join(', ')}"
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# Ensure we have path properly joined with filenames
|
|
320
|
-
prefixes_yaml = yaml_files[0]
|
|
321
|
-
dimensions_yaml = yaml_files[1]
|
|
322
|
-
units_yaml = yaml_files[2]
|
|
323
|
-
quantities_yaml = yaml_files[3]
|
|
324
|
-
unit_systems_yaml = yaml_files[4]
|
|
325
|
-
|
|
326
|
-
# Debug paths
|
|
327
|
-
if ENV["DEBUG"]
|
|
328
|
-
puts "[UnitsDB] Loading YAML files from directory: #{dir_path}"
|
|
329
|
-
puts " - #{prefixes_yaml}"
|
|
330
|
-
puts " - #{dimensions_yaml}"
|
|
331
|
-
puts " - #{units_yaml}"
|
|
332
|
-
puts " - #{quantities_yaml}"
|
|
333
|
-
puts " - #{unit_systems_yaml}"
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# Load YAML files with better error handling
|
|
337
|
-
begin
|
|
338
|
-
prefixes_hash = YAML.safe_load_file(prefixes_yaml)
|
|
339
|
-
dimensions_hash = YAML.safe_load_file(dimensions_yaml)
|
|
340
|
-
units_hash = YAML.safe_load_file(units_yaml)
|
|
341
|
-
quantities_hash = YAML.safe_load_file(quantities_yaml)
|
|
342
|
-
unit_systems_hash = YAML.safe_load_file(unit_systems_yaml)
|
|
343
|
-
rescue Errno::ENOENT => e
|
|
344
|
-
raise Errors::DatabaseFileNotFoundError,
|
|
345
|
-
"Failed to read database file: #{e.message}"
|
|
346
|
-
rescue Psych::SyntaxError => e
|
|
347
|
-
raise Errors::DatabaseFileInvalidError,
|
|
348
|
-
"Invalid YAML in database file: #{e.message}"
|
|
349
|
-
rescue StandardError => e
|
|
350
|
-
raise Errors::DatabaseLoadError, "Error loading database: #{e.message}"
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Verify all files have schema_version field
|
|
354
|
-
missing_schema = []
|
|
355
|
-
missing_schema << "prefixes.yaml" unless prefixes_hash.key?("schema_version")
|
|
356
|
-
missing_schema << "dimensions.yaml" unless dimensions_hash.key?("schema_version")
|
|
357
|
-
missing_schema << "units.yaml" unless units_hash.key?("schema_version")
|
|
358
|
-
missing_schema << "quantities.yaml" unless quantities_hash.key?("schema_version")
|
|
359
|
-
missing_schema << "unit_systems.yaml" unless unit_systems_hash.key?("schema_version")
|
|
360
|
-
|
|
361
|
-
if missing_schema.any?
|
|
362
|
-
raise Errors::DatabaseFileInvalidError,
|
|
363
|
-
"Missing schema_version in files: #{missing_schema.join(', ')}"
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
# Extract versions from each file
|
|
367
|
-
prefixes_version = prefixes_hash["schema_version"]
|
|
368
|
-
dimensions_version = dimensions_hash["schema_version"]
|
|
369
|
-
units_version = units_hash["schema_version"]
|
|
370
|
-
quantities_version = quantities_hash["schema_version"]
|
|
371
|
-
unit_systems_version = unit_systems_hash["schema_version"]
|
|
372
|
-
|
|
373
|
-
# Check if all versions match
|
|
374
|
-
versions = [
|
|
375
|
-
prefixes_version,
|
|
376
|
-
dimensions_version,
|
|
377
|
-
units_version,
|
|
378
|
-
quantities_version,
|
|
379
|
-
unit_systems_version,
|
|
380
|
-
]
|
|
381
|
-
|
|
382
|
-
unless versions.uniq.size == 1
|
|
383
|
-
version_info = {
|
|
384
|
-
"prefixes.yaml" => prefixes_version,
|
|
385
|
-
"dimensions.yaml" => dimensions_version,
|
|
386
|
-
"units.yaml" => units_version,
|
|
387
|
-
"quantities.yaml" => quantities_version,
|
|
388
|
-
"unit_systems.yaml" => unit_systems_version,
|
|
389
|
-
}
|
|
390
|
-
raise Errors::VersionMismatchError,
|
|
391
|
-
"Version mismatch in database files: #{version_info.inspect}"
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
# Check if the version is supported
|
|
395
|
-
version = versions.first
|
|
396
|
-
unless version == "2.0.0"
|
|
397
|
-
raise Errors::UnsupportedVersionError,
|
|
398
|
-
"Unsupported database version: #{version}. Only version 2.0.0 is supported."
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
combined_yaml = {
|
|
402
|
-
"schema_version" => prefixes_version,
|
|
403
|
-
"prefixes" => prefixes_hash["prefixes"],
|
|
404
|
-
"dimensions" => dimensions_hash["dimensions"],
|
|
405
|
-
"units" => units_hash["units"],
|
|
406
|
-
"quantities" => quantities_hash["quantities"],
|
|
407
|
-
"unit_systems" => unit_systems_hash["unit_systems"],
|
|
408
|
-
}.to_yaml
|
|
409
|
-
|
|
410
|
-
from_yaml(combined_yaml)
|
|
159
|
+
ReferenceValidator.validate(self)
|
|
411
160
|
end
|
|
412
161
|
|
|
413
162
|
private
|
|
414
163
|
|
|
415
|
-
#
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
collection.each_with_index do |item, index|
|
|
420
|
-
next unless item.respond_to?(:short) && item.short
|
|
164
|
+
# Resolve a caller-supplied type to a list of collection symbols,
|
|
165
|
+
# validating against `allowed`. Nil means "all allowed".
|
|
166
|
+
def scope_for(type, allowed)
|
|
167
|
+
return allowed.dup unless type
|
|
421
168
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
# Add to results if duplicates found
|
|
426
|
-
shorts.each do |short, paths|
|
|
427
|
-
next unless paths.size > 1
|
|
169
|
+
sym = type.to_sym
|
|
170
|
+
raise ArgumentError, "unknown collection: #{type.inspect}" unless allowed.include?(sym)
|
|
428
171
|
|
|
429
|
-
|
|
430
|
-
end
|
|
172
|
+
[sym]
|
|
431
173
|
end
|
|
432
174
|
|
|
433
|
-
def
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
next unless item.respond_to?(:identifiers)
|
|
438
|
-
|
|
439
|
-
# Process identifiers array for this item
|
|
440
|
-
item.identifiers.each_with_index do |identifier, id_index|
|
|
441
|
-
next unless identifier.respond_to?(:id) && identifier.id
|
|
442
|
-
|
|
443
|
-
id_key = identifier.id
|
|
444
|
-
loc = "index:#{index}:identifiers[#{id_index}]"
|
|
445
|
-
(ids[id_key] ||= []) << loc
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
# Add duplicates to results
|
|
450
|
-
ids.each do |id, paths|
|
|
451
|
-
unique_paths = paths.uniq
|
|
452
|
-
next unless unique_paths.size > 1
|
|
453
|
-
|
|
454
|
-
(results[:id][type] ||= {})[id] = unique_paths
|
|
455
|
-
end
|
|
175
|
+
def matches_text?(entity, needle)
|
|
176
|
+
entity.identifiers.any? { |i| i.id.to_s.downcase.include?(needle) } ||
|
|
177
|
+
entity.names.any? { |n| n.value.to_s.downcase.include?(needle) } ||
|
|
178
|
+
entity.short.to_s.downcase.include?(needle)
|
|
456
179
|
end
|
|
457
180
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
unit.identifiers.each do |identifier|
|
|
468
|
-
next unless identifier.id && identifier.type
|
|
469
|
-
|
|
470
|
-
# Add composite key (type:id)
|
|
471
|
-
composite_key = "#{identifier.type}:#{identifier.id}"
|
|
472
|
-
registry["units"][composite_key] = "index:#{index}"
|
|
473
|
-
|
|
474
|
-
# Also add just the ID for backward compatibility
|
|
475
|
-
registry["units"][identifier.id] = "index:#{index}"
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
# Add dimension identifiers
|
|
480
|
-
registry["dimensions"] = {}
|
|
481
|
-
dimensions.each_with_index do |dimension, index|
|
|
482
|
-
next unless dimension.respond_to?(:identifiers)
|
|
483
|
-
|
|
484
|
-
dimension.identifiers.each do |identifier|
|
|
485
|
-
next unless identifier.id && identifier.type
|
|
486
|
-
|
|
487
|
-
composite_key = "#{identifier.type}:#{identifier.id}"
|
|
488
|
-
registry["dimensions"][composite_key] = "index:#{index}"
|
|
489
|
-
registry["dimensions"][identifier.id] = "index:#{index}"
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Also track dimensions by short name
|
|
493
|
-
if dimension.respond_to?(:short) && dimension.short
|
|
494
|
-
registry["dimensions_short"] ||= {}
|
|
495
|
-
registry["dimensions_short"][dimension.short] = "index:#{index}"
|
|
496
|
-
end
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
# Add quantity identifiers
|
|
500
|
-
registry["quantities"] = {}
|
|
501
|
-
quantities.each_with_index do |quantity, index|
|
|
502
|
-
next unless quantity.respond_to?(:identifiers)
|
|
503
|
-
|
|
504
|
-
quantity.identifiers.each do |identifier|
|
|
505
|
-
next unless identifier.id && identifier.type
|
|
506
|
-
|
|
507
|
-
composite_key = "#{identifier.type}:#{identifier.id}"
|
|
508
|
-
registry["quantities"][composite_key] = "index:#{index}"
|
|
509
|
-
registry["quantities"][identifier.id] = "index:#{index}"
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
# Add prefix identifiers
|
|
514
|
-
registry["prefixes"] = {}
|
|
515
|
-
prefixes.each_with_index do |prefix, index|
|
|
516
|
-
next unless prefix.respond_to?(:identifiers)
|
|
517
|
-
|
|
518
|
-
prefix.identifiers.each do |identifier|
|
|
519
|
-
next unless identifier.id && identifier.type
|
|
520
|
-
|
|
521
|
-
composite_key = "#{identifier.type}:#{identifier.id}"
|
|
522
|
-
registry["prefixes"][composite_key] = "index:#{index}"
|
|
523
|
-
registry["prefixes"][identifier.id] = "index:#{index}"
|
|
524
|
-
end
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
# Add unit system identifiers
|
|
528
|
-
registry["unit_systems"] = {}
|
|
529
|
-
unit_systems.each_with_index do |unit_system, index|
|
|
530
|
-
next unless unit_system.respond_to?(:identifiers)
|
|
531
|
-
|
|
532
|
-
unit_system.identifiers.each do |identifier|
|
|
533
|
-
next unless identifier.id && identifier.type
|
|
534
|
-
|
|
535
|
-
composite_key = "#{identifier.type}:#{identifier.id}"
|
|
536
|
-
registry["unit_systems"][composite_key] = "index:#{index}"
|
|
537
|
-
registry["unit_systems"][identifier.id] = "index:#{index}"
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
# Also track unit systems by short name
|
|
541
|
-
if unit_system.respond_to?(:short) && unit_system.short
|
|
542
|
-
registry["unit_systems_short"] ||= {}
|
|
543
|
-
registry["unit_systems_short"][unit_system.short] = "index:#{index}"
|
|
544
|
-
end
|
|
181
|
+
def match_exact(entity, value, result)
|
|
182
|
+
if entity.short && entity.short.downcase == value.downcase
|
|
183
|
+
result[:exact] << {
|
|
184
|
+
entity: entity,
|
|
185
|
+
match_desc: "short_to_name",
|
|
186
|
+
details: "UnitsDB short '#{entity.short}' matches '#{value}'",
|
|
187
|
+
}
|
|
188
|
+
return
|
|
545
189
|
end
|
|
546
190
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
def check_dimension_references(registry, invalid_refs)
|
|
551
|
-
dimensions.each_with_index do |dimension, index|
|
|
552
|
-
next unless dimension.respond_to?(:dimension_reference) && dimension.dimension_reference
|
|
553
|
-
|
|
554
|
-
ref_id = dimension.dimension_reference
|
|
555
|
-
ref_type = "dimensions"
|
|
556
|
-
ref_path = "dimensions:index:#{index}:dimension_reference"
|
|
557
|
-
|
|
558
|
-
validate_reference(ref_id, ref_type, ref_path, registry, invalid_refs,
|
|
559
|
-
"dimensions")
|
|
191
|
+
matching_name = entity.names.find do |name|
|
|
192
|
+
name.value.to_s.downcase == value.downcase
|
|
560
193
|
end
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
def check_unit_system_references(registry, invalid_refs)
|
|
564
|
-
units.each_with_index do |unit, index|
|
|
565
|
-
next unless unit.respond_to?(:unit_system_reference) && unit.unit_system_reference
|
|
566
|
-
|
|
567
|
-
unit.unit_system_reference.each_with_index do |ref_id, idx|
|
|
568
|
-
ref_type = "unit_systems"
|
|
569
|
-
ref_path = "units:index:#{index}:unit_system_reference[#{idx}]"
|
|
194
|
+
return unless matching_name
|
|
570
195
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
196
|
+
result[:exact] << {
|
|
197
|
+
entity: entity,
|
|
198
|
+
match_desc: "name_to_name",
|
|
199
|
+
details: "UnitsDB name '#{matching_name.value}' (#{matching_name.lang}) matches '#{value}'",
|
|
200
|
+
}
|
|
575
201
|
end
|
|
576
202
|
|
|
577
|
-
def
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
unit.quantity_references.each_with_index do |ref_id, idx|
|
|
582
|
-
ref_type = "quantities"
|
|
583
|
-
ref_path = "units:index:#{index}:quantity_references[#{idx}]"
|
|
584
|
-
|
|
585
|
-
validate_reference(ref_id, ref_type, ref_path, registry,
|
|
586
|
-
invalid_refs, "units")
|
|
587
|
-
end
|
|
203
|
+
def match_symbol(entity, value, result)
|
|
204
|
+
matching_symbol = entity.symbols.find do |sym|
|
|
205
|
+
sym.ascii.to_s.downcase == value.downcase
|
|
588
206
|
end
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
def check_root_unit_references(registry, invalid_refs)
|
|
592
|
-
units.each_with_index do |unit, index|
|
|
593
|
-
next unless unit.respond_to?(:root_units) && unit.root_units
|
|
594
|
-
|
|
595
|
-
unit.root_units.each_with_index do |root_unit, idx|
|
|
596
|
-
next unless root_unit.respond_to?(:unit_reference) && root_unit.unit_reference
|
|
597
|
-
|
|
598
|
-
# Check unit reference
|
|
599
|
-
ref_id = root_unit.unit_reference
|
|
600
|
-
ref_type = "units"
|
|
601
|
-
ref_path = "units:index:#{index}:root_units.#{idx}.unit_reference"
|
|
602
|
-
|
|
603
|
-
validate_reference(ref_id, ref_type, ref_path, registry,
|
|
604
|
-
invalid_refs, "units")
|
|
207
|
+
return unless matching_symbol
|
|
605
208
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
ref_path = "units:index:#{index}:root_units.#{idx}.prefix_reference"
|
|
612
|
-
|
|
613
|
-
validate_reference(ref_id, ref_type, ref_path, registry,
|
|
614
|
-
invalid_refs, "units")
|
|
615
|
-
end
|
|
616
|
-
end
|
|
209
|
+
result[:symbol_match] << {
|
|
210
|
+
entity: entity,
|
|
211
|
+
match_desc: "symbol_match",
|
|
212
|
+
details: "UnitsDB symbol '#{matching_symbol.ascii}' matches '#{value}'",
|
|
213
|
+
}
|
|
617
214
|
end
|
|
618
215
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
#
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
# Try multiple lookup strategies
|
|
628
|
-
valid = false
|
|
629
|
-
|
|
630
|
-
# 1. Try exact composite key match
|
|
631
|
-
valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
|
|
632
|
-
|
|
633
|
-
# 2. Try just ID match if composite didn't work
|
|
634
|
-
valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
|
|
635
|
-
|
|
636
|
-
# 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
|
|
637
|
-
if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
|
|
638
|
-
registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
|
|
639
|
-
registry[ref_type].keys.any? do |k|
|
|
640
|
-
k.end_with?(":SI_#{id.sub('si-', '')}")
|
|
641
|
-
end ||
|
|
642
|
-
registry[ref_type].keys.any? do |k|
|
|
643
|
-
k.end_with?(":non-SI_#{id.sub('nonsi-', '')}")
|
|
644
|
-
end
|
|
645
|
-
)
|
|
646
|
-
# Special handling for unit_systems between unitsml and nist types
|
|
647
|
-
valid = true
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
unless valid
|
|
651
|
-
invalid_refs[file_type] ||= {}
|
|
652
|
-
invalid_refs[file_type][ref_path] =
|
|
653
|
-
{ id: id, type: type, ref_type: ref_type }
|
|
654
|
-
end
|
|
655
|
-
# Handle references that are objects with id and type in a hash
|
|
656
|
-
elsif ref_id.is_a?(Hash) && ref_id.key?("id") && ref_id.key?("type")
|
|
657
|
-
id = ref_id["id"]
|
|
658
|
-
type = ref_id["type"]
|
|
659
|
-
composite_key = "#{type}:#{id}"
|
|
660
|
-
|
|
661
|
-
# Try multiple lookup strategies
|
|
662
|
-
valid = false
|
|
663
|
-
|
|
664
|
-
# 1. Try exact composite key match
|
|
665
|
-
valid = true if registry.key?(ref_type) && registry[ref_type].key?(composite_key)
|
|
666
|
-
|
|
667
|
-
# 2. Try just ID match if composite didn't work
|
|
668
|
-
valid = true if !valid && registry.key?(ref_type) && registry[ref_type].key?(id)
|
|
669
|
-
|
|
670
|
-
# 3. Try alternate ID formats for unit systems (e.g., SI_base vs si-base)
|
|
671
|
-
if !valid && type == "unitsml" && ref_type == "unit_systems" && registry.key?(ref_type) && (
|
|
672
|
-
registry[ref_type].keys.any? { |k| k.end_with?(":#{id}") } ||
|
|
673
|
-
registry[ref_type].keys.any? do |k|
|
|
674
|
-
k.end_with?(":SI_#{id.sub('si-', '')}")
|
|
675
|
-
end ||
|
|
676
|
-
registry[ref_type].keys.any? do |k|
|
|
677
|
-
k.end_with?(":non-SI_#{id.sub('nonsi-', '')}")
|
|
678
|
-
end
|
|
679
|
-
)
|
|
680
|
-
# Special handling for unit_systems between unitsml and nist types
|
|
681
|
-
valid = true
|
|
682
|
-
end
|
|
216
|
+
class << self
|
|
217
|
+
# Load every YAML file under `dir_path` and deserialize into a
|
|
218
|
+
# Database instance scoped to `context`. The default context is
|
|
219
|
+
# auto-created via `Config.ensure_default_context!`; custom
|
|
220
|
+
# contexts must be created by the caller first.
|
|
221
|
+
def from_db(dir_path, context: Unitsdb::Config.context_id)
|
|
222
|
+
context_id = context.to_sym
|
|
223
|
+
Unitsdb::Config.ensure_default_context! if context_id == Unitsdb::Config.context_id
|
|
683
224
|
|
|
684
|
-
|
|
685
|
-
invalid_refs[file_type] ||= {}
|
|
686
|
-
invalid_refs[file_type][ref_path] =
|
|
687
|
-
{ id: id, type: type, ref_type: ref_type }
|
|
688
|
-
end
|
|
689
|
-
else
|
|
690
|
-
# Handle plain string references (legacy format)
|
|
691
|
-
valid = registry.key?(ref_type) && registry[ref_type].key?(ref_id)
|
|
225
|
+
combined_hash = Loader.load(dir_path)
|
|
692
226
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
227
|
+
Lutaml::Model::GlobalContext.with_context(context_id) do
|
|
228
|
+
if Unitsdb::Config.register_id_for(context_id)
|
|
229
|
+
from_hash(combined_hash, register: context_id)
|
|
230
|
+
else
|
|
231
|
+
from_hash(combined_hash)
|
|
232
|
+
end
|
|
696
233
|
end
|
|
697
234
|
end
|
|
698
235
|
end
|
|
699
236
|
end
|
|
237
|
+
|
|
238
|
+
Config.register_model(Database, id: :database)
|
|
700
239
|
end
|