unitsdb 2.2.6 → 2.2.7

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.
data/data/dimensions.yaml CHANGED
@@ -1862,3 +1862,13 @@ dimensions:
1862
1862
  authority: qudt
1863
1863
  uri: http://qudt.org/vocab/dimensionvector/A0E0L-2I0M0H0T0D0
1864
1864
  short: fuel_efficiency
1865
+ - identifiers:
1866
+ - type: unitsml
1867
+ id: d:currency
1868
+ currency:
1869
+ power: 1
1870
+ symbol: C
1871
+ names:
1872
+ - value: currency
1873
+ lang: en
1874
+ short: currency
data/data/quantities.yaml CHANGED
@@ -3713,3 +3713,14 @@ quantities:
3713
3713
  authority: qudt
3714
3714
  uri: http://qudt.org/vocab/quantitykind/RadialDistance
3715
3715
  short: radial_distance
3716
+ - dimension_reference:
3717
+ type: unitsml
3718
+ id: d:currency
3719
+ identifiers:
3720
+ - type: unitsml
3721
+ id: q:monetary_value
3722
+ names:
3723
+ - value: monetary value
3724
+ lang: en
3725
+ quantity_type: base
3726
+ short: monetary_value
@@ -0,0 +1,111 @@
1
+ # yaml-language-server: $schema=http://json-schema.org/draft-06/schema
2
+ ---
3
+ $schema: "http://json-schema.org/draft-06/schema#"
4
+ $id: "https://unitsml.org/schemas/currencies-schema.yaml"
5
+ title: "Currencies Database Schema"
6
+ description: "Schema for currencies.yaml - ISO 4217 currency codes as first-class entities"
7
+
8
+ type: object
9
+ required: ["schema_version", "currencies"]
10
+ properties:
11
+ schema_version:
12
+ type: string
13
+ pattern: "^\\d+\\.\\d+\\.\\d+$"
14
+ description: "Semantic version of the schema"
15
+ currencies:
16
+ type: array
17
+ description: "Array of currency definitions"
18
+ items:
19
+ $ref: "#/definitions/Currency"
20
+
21
+ definitions:
22
+ Currency:
23
+ type: object
24
+ required: ["identifiers", "names", "minor_unit_exponent", "status", "unit_system_reference", "quantity_references"]
25
+ properties:
26
+ identifiers:
27
+ type: array
28
+ description: "External identifiers for this currency (iso4217 + unitsml cur:)"
29
+ items:
30
+ $ref: "#/definitions/Identifier"
31
+ names:
32
+ type: array
33
+ description: "Multilingual names for this currency"
34
+ items:
35
+ $ref: "#/definitions/Name"
36
+ symbols:
37
+ type: array
38
+ description: "Symbol presentations (e.g. EUR carries the euro sign); the ascii form is always the ISO code"
39
+ items:
40
+ $ref: "#/definitions/SymbolPresentation"
41
+ minor_unit_exponent:
42
+ type: integer
43
+ minimum: 0
44
+ maximum: 4
45
+ description: "Decimal exponent of the minor unit (2 = cents, 0 = JPY/KRW class, 3 = BHD/KWD class, 4 = CLF class)"
46
+ status:
47
+ type: string
48
+ enum: ["active", "historic"]
49
+ description: "Whether the code is in the current ISO 4217 active list or withdrawn"
50
+ unit_system_reference:
51
+ type: array
52
+ description: "The ISO 4217 code list"
53
+ items:
54
+ $ref: "#/definitions/Reference"
55
+ quantity_references:
56
+ type: array
57
+ description: "The monetary-value quantity this currency denominates"
58
+ items:
59
+ $ref: "#/definitions/Reference"
60
+ notes:
61
+ type: string
62
+ description: "Provenance note for special codes (metals, funds, test/no-currency)"
63
+
64
+ Identifier:
65
+ type: object
66
+ required: ["type", "id"]
67
+ properties:
68
+ type:
69
+ type: string
70
+ enum: ["iso4217", "iso4217_numeric", "unitsml"]
71
+ description: "Type of identifier system"
72
+ id:
73
+ type: string
74
+ description: "Identifier value within the system (unitsml ids are cur:-prefixed; iso4217_numeric ids are the three-digit ISO numbers)"
75
+
76
+ Name:
77
+ type: object
78
+ required: ["value", "lang"]
79
+ properties:
80
+ value:
81
+ type: string
82
+ description: "Name in the specified language"
83
+ lang:
84
+ type: string
85
+ pattern: "^[a-z]{2}(-[A-Z]{2})?$"
86
+ description: "Language code (ISO 639-1, optionally with ISO 3166-1 country)"
87
+
88
+ SymbolPresentation:
89
+ type: object
90
+ required: ["unicode", "ascii", "id"]
91
+ properties:
92
+ unicode:
93
+ type: string
94
+ description: "Unicode symbol presentation"
95
+ ascii:
96
+ type: string
97
+ description: "ASCII presentation (the ISO code)"
98
+ id:
99
+ type: string
100
+ description: "Stable symbol id"
101
+
102
+ Reference:
103
+ type: object
104
+ required: ["type", "id"]
105
+ properties:
106
+ type:
107
+ type: string
108
+ description: "Type of the referenced entity"
109
+ id:
110
+ type: string
111
+ description: "Identifier of the referenced entity"
@@ -63,6 +63,9 @@ definitions:
63
63
  plane_angle:
64
64
  $ref: "#/definitions/DimensionComponent"
65
65
  description: "Plane angle dimension component"
66
+ currency:
67
+ $ref: "#/definitions/DimensionComponent"
68
+ description: "Currency dimension component (ISO 4217 monetary values)"
66
69
  references:
67
70
  type: array
68
71
  description: "External references and citations"
@@ -79,7 +79,7 @@ definitions:
79
79
  properties:
80
80
  type:
81
81
  type: string
82
- const: "nist"
82
+ enum: ["nist", "unitsml"]
83
83
  description: "Reference type indicator"
84
84
  id:
85
85
  type: string
@@ -106,7 +106,7 @@ definitions:
106
106
  authority:
107
107
  type: string
108
108
  description: "Authoritative body or organization"
109
- enum: ["si-digital-framework", "qudt", "ucum", "nist"]
109
+ enum: ["si-digital-framework", "qudt", "ucum", "nist", "iso"]
110
110
  uri:
111
111
  type: string
112
112
  format: "uri"
@@ -76,3 +76,15 @@ unit_systems:
76
76
  - value: non-SI and not acceptable
77
77
  lang: en
78
78
  short: nonsi-unacceptable
79
+ - acceptable: false
80
+ identifiers:
81
+ - type: unitsml
82
+ id: us:iso-4217
83
+ names:
84
+ - value: ISO 4217 currency codes
85
+ lang: en
86
+ references:
87
+ - type: normative
88
+ authority: iso
89
+ uri: https://www.iso.org/iso-4217-currency-codes.html
90
+ short: iso-4217
@@ -26,9 +26,9 @@ module Unitsdb
26
26
 
27
27
  # Best-effort display name: first localized name value → short → "N/A".
28
28
  def display_name
29
- name = @entity.names.first
29
+ name = @entity.names&.first
30
30
  return name.value.to_s if name&.value
31
- return @entity.short.to_s if @entity.short
31
+ return entity_short.to_s if entity_short
32
32
 
33
33
  "N/A"
34
34
  end
@@ -38,7 +38,7 @@ module Unitsdb
38
38
  puts "Entity details:"
39
39
  puts " - Type: #{type_name}"
40
40
  puts " - Name: #{display_name}"
41
- puts " - Description: #{@entity.short}" if show_short?
41
+ puts " - Description: #{entity_short}" if show_short?
42
42
  print_identifiers(header: " - Identifiers:", item_indent: " ")
43
43
  print_extras
44
44
  print_references
@@ -48,14 +48,20 @@ module Unitsdb
48
48
  def print_summary
49
49
  puts " - #{type_name}: #{display_name}"
50
50
  print_identifiers(header: " IDs:", item_indent: " ")
51
- puts " Description: #{@entity.short}" if show_short?
51
+ puts " Description: #{entity_short}" if show_short?
52
52
  puts ""
53
53
  end
54
54
 
55
55
  private
56
56
 
57
57
  def show_short?
58
- @entity.short && @entity.short != display_name
58
+ entity_short && entity_short != display_name
59
+ end
60
+
61
+ # `short` is a UnitsDB description field some entities (Currency)
62
+ # do not carry.
63
+ def entity_short
64
+ @entity.short if @entity.respond_to?(:short)
59
65
  end
60
66
 
61
67
  def print_identifiers(header:, item_indent:)
@@ -79,20 +85,24 @@ module Unitsdb
79
85
  end
80
86
 
81
87
  def print_unit_symbols
82
- return unless @entity.symbols.any?
88
+ return unless @entity.symbols.to_a.any?
83
89
 
84
90
  puts " - Symbols:"
85
91
  @entity.symbols.each { |s| puts " - #{s}" }
86
92
  end
87
93
 
88
94
  def print_references
89
- return unless @entity.references.any?
95
+ return unless entity_references.to_a.any?
90
96
 
91
97
  puts " - References:"
92
- @entity.references.each do |ref|
98
+ entity_references.each do |ref|
93
99
  puts " - #{ref.type}: #{ref.uri}"
94
100
  end
95
101
  end
102
+
103
+ def entity_references
104
+ @entity.references if @entity.respond_to?(:references)
105
+ end
96
106
  end
97
107
  end
98
108
  end
@@ -32,6 +32,8 @@ module Unitsdb
32
32
  def scan_collection(entities)
33
33
  by_uri = {}
34
34
  entities.each_with_index do |entity, index|
35
+ next unless entity.respond_to?(:references)
36
+
35
37
  refs = entity.references || []
36
38
  refs.each do |ref|
37
39
  next unless ref.authority == AUTHORITY
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unitsdb
4
+ class Currency < Lutaml::Model::Serializable
5
+ attribute :identifiers, Identifier, collection: true
6
+ attribute :names, LocalizedString, collection: true
7
+ attribute :symbols, SymbolPresentations, collection: true
8
+ attribute :minor_unit_exponent, :integer
9
+ attribute :status, :string
10
+ attribute :unit_system_reference, UnitSystemReference, collection: true
11
+ attribute :quantity_references, QuantityReference, collection: true
12
+ attribute :notes, :string
13
+ end
14
+
15
+ Config.register_model(Currency, id: :currency)
16
+ end
@@ -10,7 +10,7 @@ module Unitsdb
10
10
  # Lutaml::Model deserialization. Knows nothing about contexts or
11
11
  # registers — that's `Database.from_db`'s job.
12
12
  class Loader
13
- DATABASE_FILES = {
13
+ REQUIRED_DATABASE_FILES = {
14
14
  "prefixes" => "prefixes.yaml",
15
15
  "dimensions" => "dimensions.yaml",
16
16
  "units" => "units.yaml",
@@ -18,6 +18,15 @@ module Unitsdb
18
18
  "unit_systems" => "unit_systems.yaml",
19
19
  }.freeze
20
20
 
21
+ # Present in 2.0-era data onward; absent in older bundled data
22
+ # directories, which must keep loading. When present, the file
23
+ # participates in all validation like a required file.
24
+ OPTIONAL_DATABASE_FILES = {
25
+ "currencies" => "currencies.yaml",
26
+ }.freeze
27
+
28
+ DATABASE_FILES = REQUIRED_DATABASE_FILES.merge(OPTIONAL_DATABASE_FILES).freeze
29
+
21
30
  SUPPORTED_SCHEMA_VERSION = "2.0.0"
22
31
 
23
32
  def self.load(dir_path)
@@ -45,7 +54,7 @@ module Unitsdb
45
54
  "Database directory not found: #{@dir_path}"
46
55
  end
47
56
 
48
- missing = DATABASE_FILES.values.reject do |filename|
57
+ missing = REQUIRED_DATABASE_FILES.values.reject do |filename|
49
58
  File.exist?(File.join(@dir_path, filename))
50
59
  end
51
60
  return if missing.empty?
@@ -58,11 +67,13 @@ module Unitsdb
58
67
  if ENV["UNITSDB_DEBUG"]
59
68
  puts "[UnitsDB] Loading YAML files from directory: #{@dir_path}"
60
69
  end
61
- DATABASE_FILES.transform_values do |filename|
70
+ DATABASE_FILES.filter_map do |collection_key, filename|
62
71
  path = File.join(@dir_path, filename)
72
+ next unless File.exist?(path)
73
+
63
74
  puts " - #{path}" if ENV["UNITSDB_DEBUG"]
64
- read_yaml(path, filename)
65
- end
75
+ [collection_key, read_yaml(path, filename)]
76
+ end.to_h
66
77
  end
67
78
 
68
79
  def read_yaml(path, filename)
@@ -87,8 +98,8 @@ module Unitsdb
87
98
  end
88
99
 
89
100
  def validate_schema_versions!(documents)
90
- versions = DATABASE_FILES.each_with_object({}) do |(collection_key, filename), result|
91
- document = documents.fetch(collection_key)
101
+ versions = documents.each_with_object({}) do |(collection_key, document), result|
102
+ filename = DATABASE_FILES.fetch(collection_key)
92
103
  result[filename] = document.fetch("schema_version")
93
104
  rescue KeyError
94
105
  raise Errors::DatabaseFileInvalidError,
@@ -109,14 +120,17 @@ module Unitsdb
109
120
  end
110
121
 
111
122
  def build_database_hash(documents, schema_version)
112
- {
123
+ hash = {
113
124
  "schema_version" => schema_version,
114
125
  }.merge(
115
- DATABASE_FILES.keys.to_h do |collection_key|
116
- document = documents.fetch(collection_key)
126
+ documents.to_h do |collection_key, document|
117
127
  [collection_key, fetch_collection!(document, collection_key)]
118
128
  end,
119
129
  )
130
+ # Absent optional collections normalize to empty arrays so
131
+ # Database accessors never return nil for legacy data dirs.
132
+ OPTIONAL_DATABASE_FILES.each_key { |key| hash[key] ||= [] }
133
+ hash
120
134
  end
121
135
 
122
136
  def fetch_collection!(document, collection_key)
@@ -11,6 +11,7 @@ module Unitsdb
11
11
  # Collections whose entities carry a `symbols` attribute.
12
12
  # Used by symbol-based search/match to narrow iteration.
13
13
  SYMBOL_COLLECTIONS = %i[units prefixes].freeze
14
+ SYMBOL_SEARCH_COLLECTIONS = (SYMBOL_COLLECTIONS + %i[currencies]).freeze
14
15
 
15
16
  attribute :schema_version, :string
16
17
  attribute :version, :string
@@ -19,6 +20,7 @@ module Unitsdb
19
20
  attribute :quantities, Quantity, collection: true
20
21
  attribute :dimensions, Dimension, collection: true
21
22
  attribute :unit_systems, UnitSystem, collection: true
23
+ attribute :currencies, Currency, collection: true
22
24
 
23
25
  # Resolve a collection name (String or Symbol) to its typed Array.
24
26
  # Validates against COLLECTIONS so caller-supplied names can never
@@ -54,7 +56,7 @@ module Unitsdb
54
56
  end
55
57
  end
56
58
 
57
- # Find an entity by its specific identifier and type
59
+ # Find an entity by its identifier and type
58
60
  # @param id [String] the identifier value to search for
59
61
  # @param type [String, Symbol] the entity type (units, prefixes, quantities, etc.)
60
62
  # @return [Object, nil] the first entity with matching identifier or nil if not found
@@ -64,6 +66,34 @@ module Unitsdb
64
66
  end
65
67
  end
66
68
 
69
+ # Find a currency by its ISO 4217 alpha code (case-insensitive)
70
+ # @param code [String, Symbol] the alpha code (e.g. "EUR", :eur)
71
+ # @return [Currency, nil]
72
+ def find_by_code(code)
73
+ needle = code.to_s.upcase
74
+ currencies.find do |currency|
75
+ currency.identifiers.any? do |identifier|
76
+ identifier.type == "iso4217" && identifier.id == needle
77
+ end
78
+ end
79
+ end
80
+
81
+ # Find a currency by its ISO 4217 numeric identifier
82
+ # @param numeric [Integer, String] the numeric code (e.g. 978, "978")
83
+ # @return [Currency, nil]
84
+ def find_by_numeric(numeric)
85
+ # Base 10 explicitly: leading-zero codes ("036") are decimals, not
86
+ # octal.
87
+ needle = format("%03d", Integer(numeric.to_s, 10))
88
+ currencies.find do |currency|
89
+ currency.identifiers.any? do |identifier|
90
+ identifier.type == "iso4217_numeric" && identifier.id == needle
91
+ end
92
+ end
93
+ rescue ArgumentError
94
+ nil
95
+ end
96
+
67
97
  # Find an entity by its identifier id across all entity types
68
98
  # @param id [String] the identifier value to search for
69
99
  # @param type [String, nil] optional identifier type to match
@@ -103,23 +133,34 @@ module Unitsdb
103
133
 
104
134
  # Find entities by symbol
105
135
  # @param symbol [String] the symbol to search for (exact match, case-insensitive)
106
- # @param entity_type [String, Symbol, nil] the entity type to search (units or prefixes)
136
+ # @param entity_type [String, Symbol, nil] the entity type to search
137
+ # (units, prefixes, currencies)
107
138
  # @return [Array] entities with matching symbol
108
139
  def find_by_symbol(symbol, entity_type = nil)
109
140
  return [] unless symbol
110
141
 
111
142
  needle = symbol.downcase
112
- scope = scope_for(entity_type, SYMBOL_COLLECTIONS)
143
+ scope = scope_for(entity_type, SYMBOL_SEARCH_COLLECTIONS)
113
144
 
114
145
  scope.each_with_object([]) do |name, results|
115
146
  collection(name).each do |entity|
116
- results << entity if entity.symbols.any? do |sym|
117
- sym.ascii.to_s.downcase == needle
147
+ results << entity if entity.symbols.to_a.any? do |sym|
148
+ symbol_match?(name, sym, symbol, needle)
118
149
  end
119
150
  end
120
151
  end
121
152
  end
122
153
 
154
+ # Currency symbols carry the sign in `unicode` (€, A$) and the alpha
155
+ # code in `ascii` (EUR, AUD); units and prefixes match on ascii only.
156
+ def symbol_match?(collection_name, sym, symbol, needle)
157
+ if collection_name == :currencies
158
+ sym.unicode.to_s == symbol || sym.ascii.to_s.downcase == needle
159
+ else
160
+ sym.ascii.to_s.downcase == needle
161
+ end
162
+ end
163
+
123
164
  # Match entities by name, short, or symbol with different match types
124
165
  # @param params [Hash] match parameters
125
166
  # @option params [String] :value The value to match against
@@ -175,11 +216,11 @@ module Unitsdb
175
216
  def matches_text?(entity, needle)
176
217
  entity.identifiers.any? { |i| i.id.to_s.downcase.include?(needle) } ||
177
218
  entity.names.any? { |n| n.value.to_s.downcase.include?(needle) } ||
178
- entity.short.to_s.downcase.include?(needle)
219
+ (entity.respond_to?(:short) && entity.short.to_s.downcase.include?(needle))
179
220
  end
180
221
 
181
222
  def match_exact(entity, value, result)
182
- if entity.short && entity.short.downcase == value.downcase
223
+ if entity.respond_to?(:short) && entity.short && entity.short.downcase == value.downcase
183
224
  result[:exact] << {
184
225
  entity: entity,
185
226
  match_desc: "short_to_name",
@@ -12,6 +12,7 @@ module Unitsdb
12
12
  attribute :amount_of_substance, DimensionDetails
13
13
  attribute :luminous_intensity, DimensionDetails
14
14
  attribute :plane_angle, DimensionDetails
15
+ attribute :currency, DimensionDetails
15
16
  attribute :short, :string
16
17
  attribute :names, LocalizedString, collection: true
17
18
  attribute :references, ExternalReference, collection: true
data/lib/unitsdb/opal.rb CHANGED
@@ -26,6 +26,7 @@ require "unitsdb/si_derived_base"
26
26
  require "unitsdb/dimension_details"
27
27
  require "unitsdb/dimension"
28
28
  require "unitsdb/prefix"
29
+ require "unitsdb/currency"
29
30
  require "unitsdb/unit_system"
30
31
  require "unitsdb/quantity"
31
32
  require "unitsdb/scale"
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unitsdb
4
- VERSION = "2.2.6"
4
+ VERSION = "2.2.7"
5
5
 
6
6
  # The version of the bundled UnitsDB data (from the data/ submodule).
7
7
  # This version corresponds to a tag/commit in https://github.com/unitsml/unitsdb.
8
- UNITS_DATA_VERSION = "2.0.0"
8
+ UNITS_DATA_VERSION = "2.0.1"
9
9
  end
data/lib/unitsdb.rb CHANGED
@@ -34,6 +34,7 @@ module Unitsdb
34
34
  # Entity models
35
35
  autoload :Dimension, "unitsdb/dimension"
36
36
  autoload :Prefix, "unitsdb/prefix"
37
+ autoload :Currency, "unitsdb/currency"
37
38
  autoload :UnitSystem, "unitsdb/unit_system"
38
39
  autoload :Quantity, "unitsdb/quantity"
39
40
  autoload :Scale, "unitsdb/scale"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unitsdb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.6
4
+ version: 2.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-18 00:00:00.000000000 Z
11
+ date: 2026-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fuzzy_match
@@ -149,10 +149,12 @@ files:
149
149
  - Rakefile
150
150
  - bin/console
151
151
  - bin/setup
152
+ - data/currencies.yaml
152
153
  - data/dimensions.yaml
153
154
  - data/prefixes.yaml
154
155
  - data/quantities.yaml
155
156
  - data/scales.yaml
157
+ - data/schemas/currencies-schema.yaml
156
158
  - data/schemas/dimensions-schema.yaml
157
159
  - data/schemas/prefixes-schema.yaml
158
160
  - data/schemas/quantities-schema.yaml
@@ -199,6 +201,7 @@ files:
199
201
  - lib/unitsdb/commands/validate/si_references.rb
200
202
  - lib/unitsdb/commands/validate/ucum_references.rb
201
203
  - lib/unitsdb/config.rb
204
+ - lib/unitsdb/currency.rb
202
205
  - lib/unitsdb/database.rb
203
206
  - lib/unitsdb/database/loader.rb
204
207
  - lib/unitsdb/database/reference_validator.rb