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