sec_id 7.1.0 → 7.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ce3f9d548d4530dae973ee4ee4a7cf8e18898aaa36396423efe5ece7dd091f4
4
- data.tar.gz: 5b7950a09cfc4afd59358b230fd659a9f26a1e58e50577ed16267f4763142a7f
3
+ metadata.gz: be4a59e18a04226f78492df3eb2ec1318af8e7c99044005bda0f7b9652ac7a33
4
+ data.tar.gz: 6edd9226b4cc7b293a7fe1950f32aa0673ea0b44507b36ecc253da98f372d3c2
5
5
  SHA512:
6
- metadata.gz: 34447319dc249c2fb3b7150cc45cc4a2aa8eed8e11ab16b726d9ee73d7aa2ad01daa2eed846f0cc788fb9f955488b161a91112ea6ec683268af7fa3fdf5267e0
7
- data.tar.gz: 155fd8fe486ea96af91949ddd1adacb04310f50c820a2a5c1d98f1f55075545886c3935adfa5dacf2eedc9975ca7f1879c0e1140a8165dcd60736f24a0bcea94
6
+ metadata.gz: b4b95d04179e0aee0ff4c45dbc2e169a024d3933a2c8234de2319422d19773d34247d83b8f457317af93d536ac431406fca9455a454cc7159ae2a9bad8982955
7
+ data.tar.gz: f479c160b3229763197b1ac1fe3b7d67f8a6fc3281d3607adf59fece50e89cc8dd2160eb2d4f94d32954d136bc2e8c8998b19bfcdf1e24f4e32a2a035f9e972a
data/CHANGELOG.md CHANGED
@@ -8,64 +8,76 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [7.1.1] - 2026-07-31
12
+
13
+ Documentation and packaging only. No identifier, checksum, or public method changed.
14
+
15
+ This is the first release published from CI through RubyGems trusted publishing, signed with sigstore and pushed with a build provenance attestation.
16
+
17
+ ### Changed
18
+
19
+ - README declares the public API that Semantic Versioning covers, names the `@api private` internals excluded from it, and links where to ask a question and where to report a defect
20
+ - CHANGELOG version headings resolve to comparison links, and 2.0.1 has the entry it never had
21
+ - Gem summary punctuation matches the README tagline
22
+
11
23
  ## [7.1.0] - 2026-07-15
12
24
 
13
25
  ### Added
14
26
 
15
- - `suggest` a checksum-anchored diagnostic-repair verb that turns the checksum from a gatekeeper into a repair engine. For a structurally-valid but checksum-**failing** identifier, `SecID::<Type>.suggest(str)` and `SecID.suggest(str, types:)` enumerate the plausible single-character human errors visual/OCR homoglyph substitutions (`O`↔`0`, `I`↔`1`, `5`↔`S`, `8`↔`B`, …) and adjacent transpositions keep only those that re-validate (`valid?` is the oracle, so no false candidate ever escapes), and return them as confidence-ranked `SecID::Suggestion` value objects reporting *what changed* (edit kind, position, from/to characters, confidence tier). Available for all 9 checksum types (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI, DTI, UPI) through one shared `Suggestable` concern. Candidates rank `:high` (homoglyph) then `:medium` (transposition) then a `:checksum` recompute fallback last; there is no `:low` tier (coincidental substitutions are never generated), keeping results small and high-precision. `suggest` never mutates its input and never presents a candidate as an authoritative correction. Non-checksum types (CIK, OCC, WKN, Valoren, CFI, FISN, BIC) have no checksum oracle and are unsupported; `SecID.suggest` silently skips them. Known limitations: the mistyped character must be in the type's charset to be reachable (the vowel-free SEDOL/FIGI/DTI/UPI can't repair an `O`-for-`0`/`I`-for-`1` typo), only a single body error is in scope, and wrong-length input returns `[]`. In a seeded simulation (`benchmark/suggest_precision.rb`), every reachable single-error homoglyph or transposition that yields a checksum-failing identifier is recovered (the correct identifier is always among the returned candidates), the top-ranked body candidate ~89% of the time for homoglyph errors
27
+ - `suggest`, a checksum-anchored diagnostic-repair verb that turns the checksum from a gatekeeper into a repair engine. For a structurally-valid but checksum-**failing** identifier, `SecID::<Type>.suggest(str)` and `SecID.suggest(str, types:)` enumerate the plausible single-character human errors (visual/OCR homoglyph substitutions such as `O`↔`0`, `I`↔`1`, `5`↔`S`, `8`↔`B`, plus adjacent transpositions), keep only those that re-validate (`valid?` is the oracle, so no checksum-invalid candidate ever escapes, though a valid candidate is not necessarily the intended correction), and return them as confidence-ranked `SecID::Suggestion` value objects reporting *what changed* (edit kind, position, from/to characters, confidence tier). Available for all 9 checksum types (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI, DTI, UPI) through one shared `Suggestable` concern. Candidates rank `:high` (homoglyph) then `:medium` (transposition) then a `:checksum` recompute fallback last; there is no `:low` tier (coincidental substitutions are never generated), keeping results small and high-precision. `suggest` never mutates its input and never presents a candidate as an authoritative correction. Non-checksum types (CIK, OCC, WKN, Valoren, CFI, FISN, BIC) have no checksum oracle and are unsupported; `SecID.suggest` silently skips them. Known limitations: the mistyped character must be in the type's charset to be reachable (the vowel-free SEDOL/FIGI/DTI/UPI can't repair an `O`-for-`0`/`I`-for-`1` typo), only a single body error is in scope, and wrong-length input returns `[]`. In a seeded simulation (`benchmark/suggest_precision.rb`), every reachable single-error homoglyph or transposition that yields a checksum-failing identifier is recovered (the correct identifier is always among the returned candidates), the top-ranked body candidate ~89% of the time for homoglyph errors
16
28
 
17
29
  ## [7.0.0] - 2026-07-14
18
30
 
19
31
  ### Added
20
32
 
21
- - UPI (ISO 4914, Unique Product Identifier) support via `SecID::UPI` the gem's 16th identifier type, and the first offline UPI validator in any language. Validates the 12-character code (fixed `QZ` prefix, 9-character body, 1 check character) issued by the ANNA Derivatives Service Bureau for OTC-derivatives reporting (CFTC, EMIR, and other global mandates). The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 over the same 30-symbol alphabet as DTI (digits plus consonants; vowels and `Y` never appear), pinned empirically against DSB-issued vectors no DSB registry lookup or paywalled ISO 4914 spec required. Like DTI, `checksum`/`calculate_checksum` return a `String` rather than an `Integer`, since UPI check characters can be letters. A UPI shares the 12-character length bucket with ISIN and can double-detect (ISIN ranked first) when its digit check character also satisfies ISIN's Luhn checksum.
33
+ - UPI (ISO 4914, Unique Product Identifier) support via `SecID::UPI`: the gem's 16th identifier type, and the first offline UPI validator in any language. Validates the 12-character code (fixed `QZ` prefix, 9-character body, 1 check character) issued by the ANNA Derivatives Service Bureau for OTC-derivatives reporting (CFTC, EMIR, and other global mandates). The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 over the same 30-symbol alphabet as DTI (digits plus consonants; vowels and `Y` never appear), pinned empirically against DSB-issued vectors, with no DSB registry lookup or paywalled ISO 4914 spec required. Like DTI, `checksum`/`calculate_checksum` return a `String` rather than an `Integer`, since UPI check characters can be letters. A UPI shares the 12-character length bucket with ISIN and can double-detect (ISIN ranked first) when its digit check character also satisfies ISIN's Luhn checksum.
22
34
 
23
35
  ### Changed
24
36
 
25
- - BREAKING: Renamed the check-digit concept to **checksum** across the entire public API, because the old name was wrong on two axes DTI's and UPI's check value is a `String` (it can be a letter, not a digit), and LEI and IBAN carry a two-character check value. The instance and class methods `check_digit` → `checksum`, `calculate_check_digit` → `calculate_checksum`, and `has_check_digit?` → `has_checksum?`; the error class `SecID::InvalidCheckDigitError` → `SecID::InvalidChecksumError`; the error code `:invalid_check_digit` → `:invalid_checksum` (a **hard flip with no dual emission** dual would duplicate `errors.details` entries, so update any `errors.details`/`explain`/ActiveModel `details: true` matcher immediately); and the `:check_digit` components key → `:checksum` in `to_h`/`deconstruct_keys` across all nine checkable types. `restore`/`restore!` and the `Checkable` concern name are unchanged. No validation or checksum-arithmetic behavior changed: every identifier that was valid stays valid and every computed value is byte-identical. The old method names, the `InvalidCheckDigitError` constant, and the `:check_digit` components key remain as deprecated bridges through v7 (removed in v8); the error code is the only surface with no bridge. See MIGRATION.md for the full upgrade guide
37
+ - BREAKING: Renamed the check-digit concept to **checksum** across the entire public API, because the old name was wrong on two axes. DTI's and UPI's check value is a `String` (it can be a letter, not a digit), and LEI and IBAN carry a two-character check value. The instance and class methods `check_digit` → `checksum`, `calculate_check_digit` → `calculate_checksum`, and `has_check_digit?` → `has_checksum?`; the error class `SecID::InvalidCheckDigitError` → `SecID::InvalidChecksumError`; the error code `:invalid_check_digit` → `:invalid_checksum` (a **hard flip with no dual emission**: dual would duplicate `errors.details` entries, so update any `errors.details`/`explain`/ActiveModel `details: true` matcher immediately); and the `:check_digit` components key → `:checksum` in `to_h`/`deconstruct_keys` across all nine checkable types. `restore`/`restore!` and the `Checkable` concern name are unchanged. No validation or checksum-arithmetic behavior changed: every identifier that was valid stays valid and every computed value is byte-identical. The old method names, the `InvalidCheckDigitError` constant, and the `:check_digit` components key remain as deprecated bridges through v7 (removed in v8); the error code is the only surface with no bridge. See MIGRATION.md for the full upgrade guide
26
38
 
27
39
  ### Deprecated
28
40
 
29
- - The pre-rename check-digit names, kept as v7 bridges and removed in v8: the `check_digit` / `calculate_check_digit` / `has_check_digit?` methods (instance and class level) each warns via `Kernel#warn` on every call, visible at Ruby's default verbosity and silenceable with `-W0` / `$VERBOSE = nil` or an app-level `Warning` override; the `SecID::InvalidCheckDigitError` constant, a same-object alias of `InvalidChecksumError` so `rescue` under either name keeps working; and the `:check_digit` key, still present alongside `:checksum` in `components` / `to_h` / `deconstruct_keys`
41
+ - The pre-rename check-digit names, kept as v7 bridges and removed in v8: the `check_digit` / `calculate_check_digit` / `has_check_digit?` methods (instance and class level). Each warns via `Kernel#warn` on every call, visible at Ruby's default verbosity and silenceable with `-W0` / `$VERBOSE = nil` or an app-level `Warning` override; the `SecID::InvalidCheckDigitError` constant, a same-object alias of `InvalidChecksumError` so `rescue` under either name keeps working; and the `:check_digit` key, still present alongside `:checksum` in `components` / `to_h` / `deconstruct_keys`
30
42
 
31
43
  ## [6.1.0] - 2026-07-10
32
44
 
33
45
  ### Added
34
46
 
35
- - `SecID::Base#deconstruct_keys` every identifier now destructures in `case/in` patterns, exposing the same parsed fields `#to_h` reports under `:components`. The `keys` argument is ignored, no new keys are introduced, and validity is not part of the protocol: `SecID.parse` returning `nil` remains the validity guard, and an instance built from unparseable input binds `nil` for each component. No `deconstruct` (array-pattern) method is defined
36
- - `SecID::Base.type_key` the memoized class-level registry symbol for an identifier type, joining the existing `short_name` / `full_name` / `id_length` metadata surface. It round-trips through the registry: `SecID[SecID::ISIN.type_key] == SecID::ISIN`. It is now the single authority for that symbol; the registry, `#to_h`, `SecID.explain`, the ActiveModel validator, the detector, and the scanner all read it instead of each deriving it from the class name
47
+ - `SecID::Base#deconstruct_keys`: every identifier now destructures in `case/in` patterns, exposing the same parsed fields `#to_h` reports under `:components`. The `keys` argument is ignored, no new keys are introduced, and validity is not part of the protocol: `SecID.parse` returning `nil` remains the validity guard, and an instance built from unparseable input binds `nil` for each component. No `deconstruct` (array-pattern) method is defined
48
+ - `SecID::Base.type_key`: the memoized class-level registry symbol for an identifier type, joining the existing `short_name` / `full_name` / `id_length` metadata surface. It round-trips through the registry: `SecID[SecID::ISIN.type_key] == SecID::ISIN`. It is now the single authority for that symbol; the registry, `#to_h`, `SecID.explain`, the ActiveModel validator, the detector, and the scanner all read it instead of each deriving it from the class name
37
49
 
38
50
  ## [6.0.0] - 2026-07-08
39
51
 
40
52
  ### Added
41
53
 
42
- - Hand-written RBS type signatures under `sig/`, shipped in the gem, so consumers running Steep or an RBS-aware editor resolve sec_id's types on install. The core library is checked by Steep in strict mode with zero errors and verified at runtime against the specs via RBS::Test. New dev/test tooling (`rbs` and `steep`, `require: false` the gem stays zero-runtime-dependency) and rake tasks: `rake steep` (strict type check), `rake steep:coverage` (untyped-call gate), `rake rbs:test` (runtime signature verification), `rake rbs` (validate signatures, in the default task), and `rake sig:cfi` (regenerate the CFI dynamic-method signatures from `SecID::CFI::Tables`, guarded by a drift spec). CI gains a `types` job on Ruby 4.0. The optional ActiveModel adapter and Railtie are excluded from the typed scope
43
- - `SecID::CFI#decode` returns a frozen `SecID::CFI::Classification` value object for any valid CFI (`nil` for invalid). Its category, group, and each attribute are `Field` objects carrying the CFI letter (`#code`), semantic symbol (`#name`), and authoritative ISO 10962 label (`#label`), plus `<name>?` predicates scoped to the field's own domain so `decode.category.equity?` and `decode.attributes.voting_right.voting?` answer, while an out-of-domain predicate (e.g. `category.voting?`) raises `NoMethodError`. `#attributes` is an `Enumerable` of the attribute fields keyed by each position's group meaning (`attributes.voting_right`, nil-safe `attributes[:form]`). Ships a human-readable `#to_s` and `#to_h`/`#as_json` for serialization. `X` decodes to `:not_applicable`; pure-N/A positions are omitted
44
- - `SecID::CFI` now validates the full ISO 10962:2021 attribute matrix positions 36 are checked per-group, `Strategies` (`K`) codes require `XXXX`, pure-N/A positions accept only `X`, and the `ED` (depositary receipts on equities) cross-position rule is enforced. A new `:invalid_attribute` error code (mapped to `InvalidStructureError`) names the offending positions and group
45
- - Opt-in ActiveModel / Rails validator (`require 'sec_id/active_model'`), registered as `sec_id`, for declarative validation: `validates :isin, sec_id: { type: :isin }`. Validates a single type (`type:`), an allowlist (`types:`), or any supported type (`sec_id: true`); a bad type raises `ArgumentError` at class-load. Opt-in `normalize: true` accepts separatored input and rewrites the attribute to canonical form on success; opt-in `details: true` surfaces the specific failure reason. Auto-activates in Rails via a Railtie (no `require:`/initializer needed) and adds **no runtime dependency** nothing on the default `require 'sec_id'` path loads Rails or ActiveModel. Tested across Rails 7.2, 8.0, 8.1, and main
46
- - BIC / SWIFT code (ISO 9362) support via `SecID::BIC` validate, normalize, parse, detect, extract, and generate 8- or 11-character Business Identifier Codes. Exposes `bank_code`, `country_code`, `location_code`, and `branch_code` (`nil` for a BIC8) components. Validates the embedded country code against a frozen ISO 3166-1 / SWIFT-recognized set (`SecID::BIC.countries`), raising `InvalidStructureError` (`:invalid_country`) for unrecognized codes. Validation confirms structure and a real country code only it does not verify registration in the licensed SWIFT registry
47
- - DTI (ISO 24165, Digital Token Identifier) support via `SecID::DTI` the gem's 15th identifier type, and the first offline DTI validator in any language. Validates the ISO 7064 hybrid MOD 31,30 check character over the 30-symbol DTI alphabet (digits plus consonants; vowels and `Y` never appear), identified empirically from public registry data (1,595 of 1,596 snapshot codes plus 2 post-snapshot registrations). Bitcoin's hand-assigned code (`4H95J0R2X`, which fails the algorithm) is honored via a frozen exception map consulted by `valid?`, `restore`, and `check_digit` alike. Unlike every other check-digit type in the gem, `check_digit`/`calculate_check_digit` return a `String` rather than an `Integer`, since DTI check characters can be letters
48
- - `.generate` on all 15 identifier types plus a central `SecID.generate(:type)` dispatcher for producing syntactically valid identifiers (with correct check digits where applicable) as test fixtures accepts an optional `random:` keyword (a Ruby `Random`) for reproducible output. Generated values are valid in format only and are not real, registered securities
49
- - 100% YARD documentation coverage of the public API, enforced in CI via `rake yard:stats`. The gem now ships a `.yardopts` so `rubydoc.info` renders the public API cleanly (private internals hidden, README/CHANGELOG/LICENSE as pages) and exposes `documentation_uri` metadata (a Documentation link on the RubyGems page). Adds a `yard` dev/test dependency (`require: false` the gem stays zero-runtime-dependency) and a `rake yard` task for local HTML docs
54
+ - Hand-written RBS type signatures under `sig/`, shipped in the gem, so consumers running Steep or an RBS-aware editor resolve sec_id's types on install. The core library is checked by Steep in strict mode with zero errors and verified at runtime against the specs via RBS::Test. New dev/test tooling (`rbs` and `steep`, `require: false`; the gem stays zero-runtime-dependency) and rake tasks: `rake steep` (strict type check), `rake steep:coverage` (untyped-call gate), `rake rbs:test` (runtime signature verification), `rake rbs` (validate signatures, in the default task), and `rake sig:cfi` (regenerate the CFI dynamic-method signatures from `SecID::CFI::Tables`, guarded by a drift spec). CI gains a `types` job on Ruby 4.0. The optional ActiveModel adapter and Railtie are excluded from the typed scope
55
+ - `SecID::CFI#decode` returns a frozen `SecID::CFI::Classification` value object for any valid CFI (`nil` for invalid). Its category, group, and each attribute are `Field` objects carrying the CFI letter (`#code`), semantic symbol (`#name`), and authoritative ISO 10962 label (`#label`), plus `<name>?` predicates scoped to the field's own domain, so `decode.category.equity?` and `decode.attributes.voting_right.voting?` answer, while an out-of-domain predicate (e.g. `category.voting?`) raises `NoMethodError`. `#attributes` is an `Enumerable` of the attribute fields keyed by each position's group meaning (`attributes.voting_right`, nil-safe `attributes[:form]`). Ships a human-readable `#to_s` and `#to_h`/`#as_json` for serialization. `X` decodes to `:not_applicable`; pure-N/A positions are omitted
56
+ - `SecID::CFI` now validates the full ISO 10962:2021 attribute matrix: positions 3 to 6 are checked per-group, `Strategies` (`K`) codes require `XXXX`, pure-N/A positions accept only `X`, and the `ED` (depositary receipts on equities) cross-position rule is enforced. A new `:invalid_attribute` error code (mapped to `InvalidStructureError`) names the offending positions and group
57
+ - Opt-in ActiveModel / Rails validator (`require 'sec_id/active_model'`), registered as `sec_id`, for declarative validation: `validates :isin, sec_id: { type: :isin }`. Validates a single type (`type:`), an allowlist (`types:`), or any supported type (`sec_id: true`); a bad type raises `ArgumentError` at class-load. Opt-in `normalize: true` accepts separatored input and rewrites the attribute to canonical form on success; opt-in `details: true` surfaces the specific failure reason. Auto-activates in Rails via a Railtie (no `require:`/initializer needed) and adds **no runtime dependency**: nothing on the default `require 'sec_id'` path loads Rails or ActiveModel. Tested across Rails 7.2, 8.0, 8.1, and main
58
+ - BIC / SWIFT code (ISO 9362) support via `SecID::BIC`: validate, normalize, parse, detect, extract, and generate 8- or 11-character Business Identifier Codes. Exposes `bank_code`, `country_code`, `location_code`, and `branch_code` (`nil` for a BIC8) components. Validates the embedded country code against a frozen ISO 3166-1 / SWIFT-recognized set (`SecID::BIC.countries`), raising `InvalidStructureError` (`:invalid_country`) for unrecognized codes. Validation confirms structure and a real country code only. It does not verify registration in the licensed SWIFT registry
59
+ - DTI (ISO 24165, Digital Token Identifier) support via `SecID::DTI`: the gem's 15th identifier type, and the first offline DTI validator in any language. Validates the ISO 7064 hybrid MOD 31,30 check character over the 30-symbol DTI alphabet (digits plus consonants; vowels and `Y` never appear), identified empirically from public registry data (1,595 of 1,596 snapshot codes plus 2 post-snapshot registrations). Bitcoin's hand-assigned code (`4H95J0R2X`, which fails the algorithm) is honored via a frozen exception map consulted by `valid?`, `restore`, and `check_digit` alike. Unlike every other check-digit type in the gem, `check_digit`/`calculate_check_digit` return a `String` rather than an `Integer`, since DTI check characters can be letters
60
+ - `.generate` on all 15 identifier types plus a central `SecID.generate(:type)` dispatcher for producing syntactically valid identifiers (with correct check digits where applicable) as test fixtures. Accepts an optional `random:` keyword (a Ruby `Random`) for reproducible output. Generated values are valid in format only and are not real, registered securities
61
+ - 100% YARD documentation coverage of the public API, enforced in CI via `rake yard:stats`. The gem now ships a `.yardopts` so `rubydoc.info` renders the public API cleanly (private internals hidden, README/CHANGELOG/LICENSE as pages) and exposes `documentation_uri` metadata (a Documentation link on the RubyGems page). Adds a `yard` dev/test dependency (`require: false`; the gem stays zero-runtime-dependency) and a `rake yard` task for local HTML docs
50
62
 
51
63
  ### Changed
52
64
 
53
65
  - Internal `@api private` IBAN country-data module dissolved into the `SecID::IBAN` class, so constant names match their file paths per Ruby convention: `SecID::IBANCountryRules::COUNTRY_RULES` / `LENGTH_ONLY_COUNTRIES` are now `SecID::IBAN::COUNTRY_RULES` / `SecID::IBAN::LENGTH_ONLY_COUNTRIES`
54
- - `SecID.valid?` and `SecID.parse` are faster and allocate less `SecID.valid?` short-circuits on the first matching type instead of running a full detect-and-sort, and `SecID.parse` reuses the instance built during detection instead of instantiating the matched type a second time
55
- - **BREAKING:** `SecID::CFI.valid?` is now strict at the attribute level for all 14 categories (including the derivative categories `S`, `H`, `J`) codes with attribute letters outside the ISO 10962:2021 tables, previously accepted, are now invalid (e.g. `CFI.valid?('ESZZZZ')` returns `false`). There is no leniency option, matching every other identifier type in the gem. The ActiveModel validator inherits this strictness automatically
56
- - **BREAKING:** `SecID::CFI` group tables corrected to ISO 10962:2021 six categories carried wrong group letters. Non-listed options (`H`) are now classified by underlying (`HR` Rates, `HT` Commodities, `HE` Equity, `HC` Credit, `HF` FX, `HM` Others) instead of the copied listed-option call/put shape; `D` gains `DE` (structured products without capital protection) and corrects `DG`/`DA`/`DN` (dropping the invented `municipal_notes`); `L` becomes `LL`/`LR`/`LS`, `T` becomes `TC`/`TT`/`TR`/`TI`/…, and the phantom `FM`/`IM`/`JM`/`LM` groups are removed. Group symbols renamed accordingly (e.g. `LS` → `:securities_lending`, `TI` → `:indices`, `MM` → `:other_assets`). Real `H`-category codes that were rejected now validate; codes relying on the old wrong letters no longer do
66
+ - `SecID.valid?` and `SecID.parse` are faster and allocate less. `SecID.valid?` short-circuits on the first matching type instead of running a full detect-and-sort, and `SecID.parse` reuses the instance built during detection instead of instantiating the matched type a second time
67
+ - **BREAKING:** `SecID::CFI.valid?` is now strict at the attribute level for all 14 categories (including the derivative categories `S`, `H`, `J`). Codes with attribute letters outside the ISO 10962:2021 tables, previously accepted, are now invalid (e.g. `CFI.valid?('ESZZZZ')` returns `false`). There is no leniency option, matching every other identifier type in the gem. The ActiveModel validator inherits this strictness automatically
68
+ - **BREAKING:** `SecID::CFI` group tables corrected to ISO 10962:2021: six categories carried wrong group letters. Non-listed options (`H`) are now classified by underlying (`HR` Rates, `HT` Commodities, `HE` Equity, `HC` Credit, `HF` FX, `HM` Others) instead of the copied listed-option call/put shape; `D` gains `DE` (structured products without capital protection) and corrects `DG`/`DA`/`DN` (dropping the invented `municipal_notes`); `L` becomes `LL`/`LR`/`LS`, `T` becomes `TC`/`TT`/`TR`/`TI`/…, and the phantom `FM`/`IM`/`JM`/`LM` groups are removed. Group symbols renamed accordingly (e.g. `LS` → `:securities_lending`, `TI` → `:indices`, `MM` → `:other_assets`). Real `H`-category codes that were rejected now validate; codes relying on the old wrong letters no longer do
57
69
 
58
70
  ### Removed
59
71
 
60
- - **BREAKING:** the 12 hardcoded equity predicate helpers on `SecID::CFI` (`equity?`, `voting?`, `non_voting?`, `restricted_voting?`, `enhanced_voting?`, `restrictions?`, `no_restrictions?`, `fully_paid?`, `nil_paid?`, `partly_paid?`, `bearer?`, `registered?`). They were category-wide and semantically wrong for non-equity groups. Migration: use `cfi.decode` and its scoped field predicates `cfi.voting?` `cfi.decode.attributes.voting_right.voting?` which answer only on the field whose ISO domain defines the concept. Two names do not carry over name-for-name: migrate `cfi.equity?` to `cfi.decode.category.equity?` (or `cfi.category == :equity`, unchanged), and `cfi.no_restrictions?` to `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?` (the ISO value name)
72
+ - **BREAKING:** the 12 hardcoded equity predicate helpers on `SecID::CFI` (`equity?`, `voting?`, `non_voting?`, `restricted_voting?`, `enhanced_voting?`, `restrictions?`, `no_restrictions?`, `fully_paid?`, `nil_paid?`, `partly_paid?`, `bearer?`, `registered?`). They were category-wide and semantically wrong for non-equity groups. Migration: use `cfi.decode` and its scoped field predicates, so `cfi.voting?` becomes `cfi.decode.attributes.voting_right.voting?`, which answers only on the field whose ISO domain defines the concept. Two names do not carry over name-for-name: migrate `cfi.equity?` to `cfi.decode.category.equity?` (or `cfi.category == :equity`, unchanged), and `cfi.no_restrictions?` to `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?` (the ISO value name)
61
73
 
62
74
  ## [5.2.0] - 2026-02-24
63
75
 
64
76
  ### Added
65
77
 
66
- - `SecID.scan` and `SecID.extract` methods for finding identifiers in freeform text returns `Scanner::Match` objects (`Data.define(:type, :raw, :range, :identifier)`) with the validated identifier instance; supports `types:` filtering, hyphenated identifiers, and compound patterns (OCC with spaces, FISN with slashes)
67
- - `SecID.explain` method for debugging identifier detection returns per-type validation results showing exactly why each type matched or rejected the input
68
- - `on_ambiguous:` option for `SecID.parse` and `SecID.parse!` `:first` (default, existing behavior), `:raise` (raises `AmbiguousMatchError`), `:all` (returns array of all matching instances)
78
+ - `SecID.scan` and `SecID.extract` methods for finding identifiers in freeform text. Returns `Scanner::Match` objects (`Data.define(:type, :raw, :range, :identifier)`) with the validated identifier instance; supports `types:` filtering, hyphenated identifiers, and compound patterns (OCC with spaces, FISN with slashes)
79
+ - `SecID.explain` method for debugging identifier detection. Returns per-type validation results showing exactly why each type matched or rejected the input
80
+ - `on_ambiguous:` option for `SecID.parse` and `SecID.parse!`: `:first` (default, existing behavior), `:raise` (raises `AmbiguousMatchError`), `:all` (returns array of all matching instances)
69
81
  - `SecID::AmbiguousMatchError` exception class for ambiguous identifier detection
70
82
  - `#as_json` method on all identifier types (delegates to `#to_h`) and on `Errors` (delegates to `#details`) for JSON serialization compatibility
71
83
  - `SecID::IBAN.supported_countries` class method returning sorted array of all supported country codes
@@ -77,9 +89,9 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
77
89
 
78
90
  ### Added
79
91
 
80
- - `#==`, `#eql?`, and `#hash` methods on all identifier types two instances of the same type with the same normalized form are equal and usable as Hash keys / in Sets
81
- - `#to_h` method on all identifier types for consistent hash serialization returns `{ type:, full_id:, normalized:, valid:, components: }` with type-specific component hashes (e.g. ISIN: `country_code`, `nsin`, `check_digit`)
82
- - `#to_pretty_s` and `.to_pretty_s` display formatting methods on all identifier types, returning a human-readable string or `nil` for invalid input with type-specific formats for IBAN (4-char groups), LEI (4-char groups), ISIN (CC + NSIN + CD), CUSIP (cusip6 + issue + CD), FIGI (prefix+G + random + CD), OCC (space-separated components), and Valoren (thousands grouping)
92
+ - `#==`, `#eql?`, and `#hash` methods on all identifier types. Two instances of the same type with the same normalized form are equal and usable as Hash keys / in Sets
93
+ - `#to_h` method on all identifier types for consistent hash serialization. Returns `{ type:, full_id:, normalized:, valid:, components: }` with type-specific component hashes (e.g. ISIN: `country_code`, `nsin`, `check_digit`)
94
+ - `#to_pretty_s` and `.to_pretty_s` display formatting methods on all identifier types, returning a human-readable string or `nil` for invalid input, with type-specific formats for IBAN (4-char groups), LEI (4-char groups), ISIN (CC + NSIN + CD), CUSIP (cusip6 + issue + CD), FIGI (prefix+G + random + CD), OCC (space-separated components), and Valoren (thousands grouping)
83
95
  - Lookup service integration guides and runnable examples for OpenFIGI, SEC EDGAR, GLEIF, and Eurex APIs (`docs/guides/`, `examples/`)
84
96
  - GitHub community standards files: Code of Conduct, Contributing guide, Security policy, issue templates, and PR template
85
97
 
@@ -117,15 +129,15 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
117
129
 
118
130
  ### Removed
119
131
 
120
- - Class-level `.normalize!` on CIK, OCC, and Valoren replaced by `.normalize`
132
+ - Class-level `.normalize!` on CIK, OCC, and Valoren, replaced by `.normalize`
121
133
  - `upcase` keyword parameter from `Base#parse`
122
134
  - `#valid_format?` instance method (now private) and `.valid_format?` class method
123
- - `OCC#full_symbol` method use `#full_id` instead
135
+ - `OCC#full_symbol` method. Use `#full_id` instead
124
136
 
125
137
  ### Fixed
126
138
 
127
- - `to_str` now always returns the same value as `to_s` across all identifier types previously LEI, IBAN, and Checkable identifiers could return divergent strings due to Ruby `alias` resolving to the parent class method
128
- - OCC `#date` memoization for invalid dates previously re-attempted parsing on every call instead of caching `nil`
139
+ - `to_str` now always returns the same value as `to_s` across all identifier types. Previously LEI, IBAN, and Checkable identifiers could return divergent strings due to Ruby `alias` resolving to the parent class method
140
+ - OCC `#date` memoization for invalid dates. Previously it re-attempted parsing on every call instead of caching `nil`
129
141
  - LEI `restore` and `to_s` now correctly pad single-digit check digits to 2 characters
130
142
  - `Valoren#to_isin` no longer mutates the source instance
131
143
 
@@ -159,7 +171,7 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
159
171
  ### Fixed
160
172
 
161
173
  - Allow Crown Dependencies (GG, IM, JE) and Overseas Territories (FK) in SEDOL/ISIN conversions ([@wtn](https://github.com/wtn), [#117](https://github.com/svyatov/sec_id/pull/117))
162
- - Removed BR (Brazil) from CGS country codes Brazil never used CINS numbers and Brazilian ISINs cannot be converted to CUSIP ([@wtn](https://github.com/wtn), [#110](https://github.com/svyatov/sec_id/pull/110))
174
+ - Removed BR (Brazil) from CGS country codes. Brazil never used CINS numbers and Brazilian ISINs cannot be converted to CUSIP ([@wtn](https://github.com/wtn), [#110](https://github.com/svyatov/sec_id/pull/110))
163
175
 
164
176
  ## [4.3.0] - 2025-01-13
165
177
 
@@ -174,7 +186,7 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
174
186
  - Refactored CIK and OCC to inherit from Base class with `has_check_digit?` hook for cleaner architecture
175
187
  - Added `Normalizable` module for consistent `normalize!` class method across identifiers
176
188
  - Added `validate_format_for_calculation!` helper method to Base class to reduce code duplication
177
- - Added comprehensive YARD documentation to all classes (public and private methods)
189
+ - Added YARD documentation to all classes (public and private methods)
178
190
  - Applied Stepdown Rule to method ordering throughout codebase
179
191
  - Created shared RSpec examples for edge cases (nil, empty, whitespace inputs)
180
192
  - Created shared RSpec examples for check-digit and normalization identifiers
@@ -232,6 +244,10 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
232
244
  - TravisCI config updated: dropped Ruby 2.3 and 2.4, added Ruby 2.7
233
245
  - Rubocop's Ruby target version changed to 2.5
234
246
 
247
+ ## [2.0.1] - 2019-02-04
248
+
249
+ Gem metadata only: corrected the gemspec description and README wording. No library change.
250
+
235
251
  ## [2.0.0] - 2019-02-03
236
252
 
237
253
  ### Added
@@ -268,3 +284,24 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
268
284
  ### Added
269
285
 
270
286
  - ISIN numbers support: `SecID::ISIN`
287
+
288
+ [Unreleased]: https://github.com/svyatov/sec_id/compare/v7.1.1...HEAD
289
+ [7.1.1]: https://github.com/svyatov/sec_id/compare/v7.1.0...v7.1.1
290
+ [7.1.0]: https://github.com/svyatov/sec_id/compare/v7.0.0...v7.1.0
291
+ [7.0.0]: https://github.com/svyatov/sec_id/compare/v6.1.0...v7.0.0
292
+ [6.1.0]: https://github.com/svyatov/sec_id/compare/v6.0.0...v6.1.0
293
+ [6.0.0]: https://github.com/svyatov/sec_id/compare/v5.2.0...v6.0.0
294
+ [5.2.0]: https://github.com/svyatov/sec_id/compare/v5.1.0...v5.2.0
295
+ [5.1.0]: https://github.com/svyatov/sec_id/compare/v5.0.0...v5.1.0
296
+ [5.0.0]: https://github.com/svyatov/sec_id/compare/v4.4.1...v5.0.0
297
+ [4.4.1]: https://github.com/svyatov/sec_id/compare/v4.4.0...v4.4.1
298
+ [4.4.0]: https://github.com/svyatov/sec_id/compare/v4.3.0...v4.4.0
299
+ [4.3.0]: https://github.com/svyatov/sec_id/compare/v4.2.0...v4.3.0
300
+ [4.2.0]: https://github.com/svyatov/sec_id/compare/v4.1.0...v4.2.0
301
+ [4.1.0]: https://github.com/svyatov/sec_id/compare/v4.0.0...v4.1.0
302
+ [4.0.0]: https://github.com/svyatov/sec_id/compare/v3.0.0...v4.0.0
303
+ [3.0.0]: https://github.com/svyatov/sec_id/compare/v2.0.1...v3.0.0
304
+ [2.0.1]: https://github.com/svyatov/sec_id/compare/v2.0.0...v2.0.1
305
+ [2.0.0]: https://github.com/svyatov/sec_id/compare/v1.1.0...v2.0.0
306
+ [1.1.0]: https://github.com/svyatov/sec_id/compare/v1.0.0...v1.1.0
307
+ [1.0.0]: https://github.com/svyatov/sec_id/releases/tag/v1.0.0
data/README.md CHANGED
@@ -1,19 +1,27 @@
1
- # SecID [![Gem Version](https://badge.fury.io/rb/sec_id.svg)](https://rubygems.org/gems/sec_id) [![CI](https://github.com/svyatov/sec_id/actions/workflows/main.yml/badge.svg)](https://github.com/svyatov/sec_id/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/svyatov/sec_id/graph/badge.svg?token=VV49EMQIIC)](https://codecov.io/gh/svyatov/sec_id) [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/sec_id) [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D.svg)](https://www.ruby-lang.org) [![Types: RBS](https://img.shields.io/badge/types-RBS-8A2BE2.svg)](https://github.com/svyatov/sec_id/tree/main/sig)
1
+ # SecID
2
2
 
3
- > A Ruby toolkit for securities identifiers validate, parse, normalize, detect, convert, generate, classify, and repair.
3
+ A Ruby toolkit for securities identifiers: validate, parse, normalize, detect, convert, generate, classify, and repair.
4
4
 
5
- ## Table of Contents
5
+ [![Gem Version](https://badge.fury.io/rb/sec_id.svg)](https://rubygems.org/gems/sec_id) [![CI](https://github.com/svyatov/sec_id/actions/workflows/main.yml/badge.svg)](https://github.com/svyatov/sec_id/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/svyatov/sec_id/graph/badge.svg?token=VV49EMQIIC)](https://codecov.io/gh/svyatov/sec_id) [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/sec_id) [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D.svg)](https://www.ruby-lang.org) [![Types: RBS](https://img.shields.io/badge/types-RBS-8A2BE2.svg)](https://github.com/svyatov/sec_id/tree/main/sig)
6
6
 
7
- - [Supported Ruby Versions](#supported-ruby-versions)
7
+ - **Nothing to call, nothing to sign up for.** SecID validates all 16 identifier standards offline. No registry lookup, no API key, no network call anywhere in [`lib/`](lib/).
8
+ - **9 of the 16 carry a checksum.** SecID validates it, restores a missing one, and repairs a wrong one.
9
+ - **Zero runtime dependencies.** Runs on Ruby 3.2 or newer.
10
+ - **Rails, if you want it.** An opt-in ActiveModel validator adds `validates :isin, sec_id: true`. It stays off the default require path, so the dependency count stays at zero.
11
+ - **Typed.** Ships RBS signatures for the whole public API, checked by Steep in strict mode.
12
+
13
+ ## Table of contents
14
+
15
+ - [Supported Ruby versions](#supported-ruby-versions)
8
16
  - [Installation](#installation)
9
- - [Supported Standards and Usage](#supported-standards-and-usage)
10
- - [Metadata Registry](#metadata-registry) - enumerate, filter, look up, and detect identifier types
11
- - [Text Scanning](#text-scanning) - find identifiers in freeform text
12
- - [Debugging Detection](#debugging-detection) - understand why strings match or don't
13
- - [Structured Validation](#structured-validation) - detailed error codes and messages
14
- - [Pattern Matching](#pattern-matching) - destructure identifiers with `case/in`
15
- - [Generating Test Fixtures](#generating-test-fixtures) - produce valid identifiers for tests
16
- - [Repairing Typos](#repairing-typos) - suggest corrections for checksum-failing identifiers
17
+ - [Supported standards and usage](#supported-standards-and-usage)
18
+ - [Metadata registry](#metadata-registry) - enumerate, filter, look up, and detect identifier types
19
+ - [Text scanning](#text-scanning) - find identifiers in freeform text
20
+ - [Debugging detection](#debugging-detection) - understand why strings match or don't
21
+ - [Structured validation](#structured-validation) - detailed error codes and messages
22
+ - [Pattern matching](#pattern-matching) - destructure identifiers with `case/in`
23
+ - [Generating test fixtures](#generating-test-fixtures) - produce valid identifiers for tests
24
+ - [Repairing typos](#repairing-typos) - suggest corrections for checksum-failing identifiers
17
25
  - [ISIN](#isin) - International Securities Identification Number
18
26
  - [CUSIP](#cusip) - Committee on Uniform Securities Identification Procedures
19
27
  - [CEI](#cei) - CUSIP Entity Identifier
@@ -30,16 +38,17 @@
30
38
  - [BIC](#bic) - Business Identifier Code / SWIFT code
31
39
  - [DTI](#dti) - Digital Token Identifier
32
40
  - [UPI](#upi) - Unique Product Identifier
33
- - [ActiveModel / Rails Validator](#activemodel--rails-validator) - declarative `validates :isin, sec_id: {...}`
34
- - [Lookup Service Integration](#lookup-service-integration)
35
- - [Type Signatures (RBS)](#type-signatures-rbs)
41
+ - [ActiveModel / Rails validator](#activemodel--rails-validator) - declarative `validates :isin, sec_id: {...}`
42
+ - [Lookup service integration](#lookup-service-integration)
43
+ - [Type signatures (RBS)](#type-signatures-rbs)
36
44
  - [Development](#development)
45
+ - [Support and status](#support-and-status)
37
46
  - [Contributing](#contributing)
38
47
  - [Changelog](#changelog)
39
48
  - [Versioning](#versioning)
40
49
  - [License](#license)
41
50
 
42
- ## Supported Ruby Versions
51
+ ## Supported Ruby versions
43
52
 
44
53
  Ruby 3.2+ is required.
45
54
 
@@ -65,7 +74,7 @@ gem install sec_id
65
74
 
66
75
  **Upgrading from v4?** See [MIGRATION.md](MIGRATION.md) for a step-by-step guide.
67
76
 
68
- ## Supported Standards and Usage
77
+ ## Supported standards and usage
69
78
 
70
79
  All identifier classes provide `valid?`, `errors`, `validate`, `validate!` methods at both class and instance levels.
71
80
 
@@ -90,7 +99,7 @@ SecID::ISIN.new('INVALID').to_h
90
99
  # valid: false, components: { country_code: nil, nsin: nil, checksum: nil } }
91
100
  ```
92
101
 
93
- **All identifiers** support value equality two instances of the same type with the same normalized form are equal:
102
+ **All identifiers** support value equality. Two instances of the same type with the same normalized form are equal:
94
103
 
95
104
  ```ruby
96
105
  a = SecID::ISIN.new('US5949181045')
@@ -109,7 +118,7 @@ Set.new([a, b]).size # => 1
109
118
  - `restore!` / `.restore!` - restores checksum in place and returns `self` / instance
110
119
  - `checksum` / `calculate_checksum` - calculates and returns the checksum
111
120
 
112
- ### Metadata Registry
121
+ ### Metadata registry
113
122
 
114
123
  All identifier classes are registered automatically and can be enumerated, filtered, and looked up by symbol key:
115
124
 
@@ -165,7 +174,7 @@ SecID.parse('514000', on_ambiguous: :all) # => [#<SecID::WKN>, #<SecID:
165
174
  SecID.parse('US5949181045', on_ambiguous: :raise) # => #<SecID::ISIN> (unambiguous, no error)
166
175
  ```
167
176
 
168
- ### Text Scanning
177
+ ### Text scanning
169
178
 
170
179
  Find identifiers embedded in freeform text:
171
180
 
@@ -191,12 +200,12 @@ match.identifier.normalized # => "US5949181045"
191
200
 
192
201
  > **Known limitations:** Format-only types (CIK, Valoren, WKN, BIC) can false-positive on
193
202
  > common numbers and short words in prose (a BIC8 is 8 letters with a valid country code in the
194
- > middle) use the `types:` filter to restrict scanning when
203
+ > middle). Use the `types:` filter to restrict scanning when
195
204
  > this is a concern. Identifiers prefixed with special characters (e.g. `#US5949181045`) may be
196
205
  > consumed as a single token by CUSIP's `*@#` character class and fail validation, preventing
197
206
  > the embedded identifier from being found.
198
207
 
199
- ### Debugging Detection
208
+ ### Debugging detection
200
209
 
201
210
  Understand why a string matches or doesn't match specific identifier types:
202
211
 
@@ -210,7 +219,7 @@ isin[:errors].first[:error] # => :invalid_checksum
210
219
  SecID.explain('US5949181045', types: %i[isin cusip])
211
220
  ```
212
221
 
213
- ### Structured Validation
222
+ ### Structured validation
214
223
 
215
224
  All identifier classes provide a Rails-like `#errors` API for detailed error reporting:
216
225
 
@@ -266,7 +275,7 @@ SecID::FIGI.new('BSG000BLNNH6').validate!
266
275
  isin = SecID::ISIN.validate!('US5949181045') # => #<SecID::ISIN>
267
276
  ```
268
277
 
269
- ### Pattern Matching
278
+ ### Pattern matching
270
279
 
271
280
  Every identifier destructures in `case/in` via its parsed components. Since `SecID.parse` returns `nil` for
272
281
  anything invalid, an `in nil` branch is a complete validity guard:
@@ -306,11 +315,11 @@ CIK, WKN, and Valoren have no sub-fields: they match a bare `in SecID::CIK` but
306
315
 
307
316
  Watch the `:type` key: on a `SecID::Match` it is the registry symbol (`:occ`), while on an `OCC` instance it is the
308
317
  OSI option type (`'C'` or `'P'`), so `in SecID::OCC[type: :occ]` never matches. Other types have no `:type` component
309
- at all `#to_h`'s envelope keys (`:type`, `:full_id`, `:normalized`, `:valid`) are not part of the pattern surface.
318
+ at all. `#to_h`'s envelope keys (`:type`, `:full_id`, `:normalized`, `:valid`) are not part of the pattern surface.
310
319
 
311
- ### Generating Test Fixtures
320
+ ### Generating test fixtures
312
321
 
313
- Generate syntactically valid identifiers with correct checksum where applicable for use as test fixtures. Available per class and via the central dispatcher:
322
+ Generate syntactically valid identifiers, with correct checksum where applicable, for use as test fixtures. Available per class and via the central dispatcher:
314
323
 
315
324
  ```ruby
316
325
  SecID::ISIN.generate # => #<SecID::ISIN ...>
@@ -324,17 +333,17 @@ SecID.generate(:nope) # => raises ArgumentError: Unknown identifier type
324
333
  SecID::LEI.generate(random: Random.new(42)) == SecID::LEI.generate(random: Random.new(42)) # => true
325
334
  ```
326
335
 
327
- > **Generated identifiers are valid in format only they are not real, registered securities.**
336
+ > **Generated identifiers are valid in format only. They are not real, registered securities.**
328
337
  > Country codes, FIGI prefixes, OCC expiry dates, and CFI category/group/attribute choices are
329
338
  > randomly selected (from the values each standard permits) and do not map to real-world
330
339
  > instruments. Use them as test fixtures, not as references to actual securities.
331
340
 
332
- ### Repairing Typos
341
+ ### Repairing typos
333
342
 
334
- Turn the checksum from a gatekeeper into a repair engine. For a checksum-**failing** identifier, `suggest` enumerates the plausible single-character human errors visual/OCR homoglyph substitutions (`O`↔`0`, `I`↔`1`, `5`↔`S`, `8`↔`B`, …) and adjacent transpositions keeps only the edits that re-validate, and returns them as confidence-ranked `SecID::Suggestion` candidates that report *what changed*. Available for all 9 checksum types (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI, DTI, UPI) per class and via the central dispatcher:
343
+ Turn the checksum from a gatekeeper into a repair engine. For a checksum-**failing** identifier, `suggest` enumerates the plausible single-character human errors (visual/OCR homoglyph substitutions such as `O`↔`0`, `I`↔`1`, `5`↔`S`, `8`↔`B`, plus adjacent transpositions), keeps only the edits that re-validate, and returns them as confidence-ranked `SecID::Suggestion` candidates that report *what changed*. Available for all 9 checksum types (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI, DTI, UPI) per class and via the central dispatcher:
335
344
 
336
345
  ```ruby
337
- # A letter O typed where a 0 belongs 'US5949181O45' should be 'US5949181045'
346
+ # A letter O typed where a 0 belongs: 'US5949181O45' should be 'US5949181045'
338
347
  top = SecID::ISIN.suggest('US5949181O45').first
339
348
  top.to_s # => 'US5949181045' (the corrected identifier)
340
349
  top.edit # => :substitution
@@ -342,27 +351,28 @@ top.position # => 9
342
351
  top.from # => 'O'
343
352
  top.to # => '0'
344
353
  top.confidence # => :high
345
- top.identifier # => #<SecID::ISIN ...> (parsed and valid call .country_code, .to_h, etc.)
354
+ top.identifier # => #<SecID::ISIN ...> (parsed and valid; call .country_code, .to_h, etc.)
346
355
 
347
356
  # Module-level: infers every format-compatible checksum type (like parse / detect)
348
357
  SecID.suggest('US5949181O45') # => [#<SecID::Suggestion type=:isin ...>, ...]
349
358
  SecID.suggest('US5949181O45', types: [:isin]) # => restrict to specific types
350
359
  ```
351
360
 
352
- Each candidate carries the corrected `identifier` (a parsed, valid instance), the `edit` kind, its `position`, the `from`/`to` characters, and a `confidence` tier. Candidates are ranked by confidence: `:high` homoglyph substitutions first, then `:medium` adjacent transpositions, then the `:checksum` recompute (body assumed correct, wrong check character) last as a fallback hypothesis. **There is no `:low` tier** coincidental substitutions that merely satisfy the checksum are never generated, keeping the result small and high-precision.
361
+ Each candidate carries the corrected `identifier` (a parsed, valid instance), the `edit` kind, its `position`, the `from`/`to` characters, and a `confidence` tier. Candidates are ranked by confidence: `:high` homoglyph substitutions first, then `:medium` adjacent transpositions, then the `:checksum` recompute (body assumed correct, wrong check character) last as a fallback hypothesis. **There is no `:low` tier.** Coincidental substitutions that merely satisfy the checksum are never generated, keeping the result small and high-precision.
353
362
 
354
363
  > **`suggest` returns candidates, never authoritative corrections, and never mutates its input.**
355
- > Every returned candidate fully re-validates (`valid?` is the oracle), so no false candidate escapes
356
- > but the `confidence` tier is how *you* decide what to trust. This matters for financial identifiers.
364
+ > Every returned candidate fully re-validates (`valid?` is the oracle), so no checksum-invalid candidate escapes,
365
+ > but a valid candidate is not necessarily *the* correction, so the `confidence` tier is how *you* decide what to
366
+ > trust. This matters for financial identifiers.
357
367
 
358
368
  Notes and limitations:
359
369
 
360
- - **Never empty for structurally-valid input** a parseable but checksum-failing identifier always yields at least the `:checksum` fallback. Only wrong-length or illegal-charset input (which fails the format gate) returns `[]`.
361
- - **Vowel-free reachability** SEDOL, FIGI, DTI, and UPI exclude vowels from their charset, so an `O`-for-`0` or `I`-for-`1` typo is unparseable and therefore unrepairable (it fails the format gate before enumeration). The mistyped character must be *in* the type's charset to be reachable.
362
- - **Single body error only** two or more wrong body characters, or a dropped/doubled character (insertion/deletion), are out of scope; such input returns only the `:checksum` fallback, never additional body candidates.
363
- - **Non-checksum types are unsupported** CIK, OCC, WKN, Valoren, CFI, FISN, and BIC have no checksum oracle, so they have no `suggest`; `SecID.suggest` silently skips them.
370
+ - **Never empty for structurally-valid input.** A parseable but checksum-failing identifier always yields at least the `:checksum` fallback. Only wrong-length or illegal-charset input (which fails the format gate), or an already-valid identifier (nothing to repair), returns `[]`.
371
+ - **Vowel-free reachability.** SEDOL, FIGI, DTI, and UPI exclude vowels from their charset, so an `O`-for-`0` or `I`-for-`1` typo is unparseable and therefore unrepairable (it fails the format gate before enumeration). The mistyped character must be *in* the type's charset to be reachable.
372
+ - **Single body error only.** Two or more wrong body characters, or a dropped/doubled character (insertion/deletion), are out of scope; such input returns only the `:checksum` fallback, never additional body candidates.
373
+ - **Non-checksum types are unsupported.** CIK, OCC, WKN, Valoren, CFI, FISN, and BIC have no checksum oracle, so they have no `suggest`; `SecID.suggest` silently skips them.
364
374
 
365
- **Precision** (simulated over 1,000 seeded samples per checksum type see [`benchmark/suggest_precision.rb`](benchmark/suggest_precision.rb)): every reachable, single-error homoglyph or transposition that yields a checksum-failing identifier is recovered the correct identifier is **always** among the returned candidates (100%), and is the top-ranked body candidate ~89% of the time for homoglyph errors. In-charset homoglyph reachability averages ~96% (100% for the letter-permitting types, lower for the four vowel-free ones).
375
+ **Precision** (simulated over 1,000 seeded samples per checksum type; see [`benchmark/suggest_precision.rb`](benchmark/suggest_precision.rb)): every reachable, single-error homoglyph or transposition that yields a checksum-failing identifier is recovered: the correct identifier is **always** among the returned candidates (100%), and is the top-ranked body candidate ~89% of the time for homoglyph errors. In-charset homoglyph reachability averages ~96% (100% for the letter-permitting types, lower for the four vowel-free ones).
366
376
 
367
377
  ### ISIN
368
378
 
@@ -702,7 +712,7 @@ SecID::CFI.new('QQXXXX').decode # => nil (decode returns nil for an invalid
702
712
 
703
713
  CFI is validated strictly against the ISO 10962:2021 code tables for all 14 categories: the category (position 1), the group (position 2), and every attribute (positions 3-6) must be a value the standard defines for that group. `X` means "not applicable" and is accepted in every position; `Strategies` (`K`) codes carry no attributes and require `XXXX`. An impermissible attribute letter raises `InvalidStructureError` (`:invalid_attribute`).
704
714
 
705
- > **Migration from &lt; 6.0:** the old category-wide equity predicates (`cfi.voting?`, `cfi.fully_paid?`, …) are removed. Use `cfi.decode` and its scoped fields instead a predicate now lives on the field whose domain defines it: `cfi.voting?` → `cfi.decode.attributes.voting_right.voting?`. Two do not map name-for-name: `cfi.equity?` → `cfi.decode.category.equity?` (or `cfi.category == :equity`), and `cfi.no_restrictions?` → `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?`. Several group letters and symbols also changed to match ISO 10962:2021 (e.g. non-listed options `H` are now classified by underlying, and `LS` → `:securities_lending`, `TI` → `:indices`).
715
+ > **Migration from &lt; 6.0:** the old category-wide equity predicates (`cfi.voting?`, `cfi.fully_paid?`, …) are removed. Use `cfi.decode` and its scoped fields instead. A predicate now lives on the field whose domain defines it: `cfi.voting?` → `cfi.decode.attributes.voting_right.voting?`. Two do not map name-for-name: `cfi.equity?` → `cfi.decode.category.equity?` (or `cfi.category == :equity`), and `cfi.no_restrictions?` → `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?`. Several group letters and symbols also changed to match ISO 10962:2021 (e.g. non-listed options `H` are now classified by underlying, and `LS` → `:securities_lending`, `TI` → `:indices`).
706
716
 
707
717
  ```ruby
708
718
  # Introspect valid codes
@@ -757,7 +767,7 @@ SecID::BIC.valid?('DEUTZZFF') # => false ('ZZ' is not a recognized country
757
767
  SecID::BIC.countries # => ['AD', 'AE', 'AF', ...] (sorted, includes 'XK')
758
768
  ```
759
769
 
760
- BIC validation confirms structure and a real country code only. It does **not** verify that the institution, location, or branch corresponds to a registered SWIFT participant that requires the licensed SWIFT registry.
770
+ BIC validation confirms structure and a real country code only. It does **not** verify that the institution, location, or branch corresponds to a registered SWIFT participant. That requires the licensed SWIFT registry.
761
771
 
762
772
  ### DTI
763
773
 
@@ -781,7 +791,7 @@ dti.restore! # => #<SecID::DTI> (mutates instance)
781
791
  dti.calculate_checksum # => 'S'
782
792
  ```
783
793
 
784
- DTI accepts exactly 9 characters: an 8-character base (first character never `0`) plus 1 check character, both drawn from a 30-symbol alphabet digits `0`-`9` and consonants (vowels and `Y` never appear). Unlike most checksum types in this gem, `checksum` and `calculate_checksum` return a `String`, not an `Integer` (as does UPI). The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 no registry lookup or paywalled ISO 24165-1 spec required.
794
+ DTI accepts exactly 9 characters: an 8-character base (first character never `0`) plus 1 check character, both drawn from a 30-symbol alphabet: digits `0`-`9` and consonants (vowels and `Y` never appear). Unlike most checksum types in this gem, `checksum` and `calculate_checksum` return a `String`, not an `Integer` (as does UPI). The check character is computed fully offline via ISO 7064 hybrid MOD 31,30, with no registry lookup or paywalled ISO 24165-1 spec required.
785
795
 
786
796
  > **Grandfathered code:** Bitcoin's registered code (`4H95J0R2X`) predates the algorithm and fails the MOD 31,30 computation (which yields `4H95J0R2T`). A frozen exception map honors the registry's assignment across `valid?`, `restore`, and `checksum` alike:
787
797
  >
@@ -812,15 +822,15 @@ upi.restore! # => #<SecID::UPI> (mutates instance)
812
822
  upi.calculate_checksum # => '2'
813
823
  ```
814
824
 
815
- UPI accepts exactly 12 characters: a fixed `QZ` prefix, a 9-character body, and 1 check character, all drawn from the same 30-symbol alphabet as DTI digits `0`-`9` and consonants (vowels and `Y` never appear). Like DTI, `checksum` and `calculate_checksum` return a `String`, not an `Integer`. The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 over the 11 preceding characters no DSB registry lookup or paywalled ISO 4914 spec required.
825
+ UPI accepts exactly 12 characters: a fixed `QZ` prefix, a 9-character body, and 1 check character, all drawn from the same 30-symbol alphabet as DTI: digits `0`-`9` and consonants (vowels and `Y` never appear). Like DTI, `checksum` and `calculate_checksum` return a `String`, not an `Integer`. The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 over the 11 preceding characters, with no DSB registry lookup or paywalled ISO 4914 spec required.
816
826
 
817
- > **Coexistence with ISIN:** a UPI shares the 12-character length bucket with ISIN. A UPI whose digit check character also satisfies ISIN's Luhn detects as both (`SecID.detect('QZXKR05S3DL1') # => [:isin, :upi]`), with ISIN ranked first; `SecID.parse(..., on_ambiguous: :raise)` surfaces the collision. UPI validation itself is fully offline and existence is **not** verified that requires the licensed DSB registry.
827
+ > **Coexistence with ISIN:** a UPI shares the 12-character length bucket with ISIN. A UPI whose digit check character also satisfies ISIN's Luhn detects as both (`SecID.detect('QZXKR05S3DL1') # => [:isin, :upi]`), with ISIN ranked first; `SecID.parse(..., on_ambiguous: :raise)` surfaces the collision. UPI validation itself is fully offline and existence is **not** verified. That requires the licensed DSB registry.
818
828
 
819
- ## ActiveModel / Rails Validator
829
+ ## ActiveModel / Rails validator
820
830
 
821
- SecID ships an opt-in [ActiveModel](https://api.rubyonrails.org/classes/ActiveModel/Validations.html) validator, registered as `sec_id`, for declarative validation of any supported identifier type. It adds **no runtime dependency** `require 'sec_id'` loads none of it, and ActiveModel is a development/test dependency only.
831
+ SecID ships an opt-in [ActiveModel](https://api.rubyonrails.org/classes/ActiveModel/Validations.html) validator, registered as `sec_id`, for declarative validation of any supported identifier type. It adds **no runtime dependency**: `require 'sec_id'` loads none of it, and ActiveModel is a development/test dependency only.
822
832
 
823
- **In Rails it just works.** A Railtie loads the validator automatically after the framework boots, so `gem 'sec_id'` in your `Gemfile` is enough no `require:` option and no initializer:
833
+ **In Rails it just works.** A Railtie loads the validator automatically after the framework boots, so `gem 'sec_id'` in your `Gemfile` is enough, with no `require:` option and no initializer:
824
834
 
825
835
  ```ruby
826
836
  class Security < ApplicationRecord
@@ -837,13 +847,13 @@ require 'sec_id/active_model'
837
847
  ### Validation modes
838
848
 
839
849
  ```ruby
840
- # Single type the value must be a valid ISIN
850
+ # Single type: the value must be a valid ISIN
841
851
  validates :isin, sec_id: { type: :isin }
842
852
 
843
- # Allowlist valid as at least one of the listed types
853
+ # Allowlist: valid as at least one of the listed types
844
854
  validates :ref, sec_id: { types: %i[isin cusip] }
845
855
 
846
- # Type-agnostic valid as any supported type
856
+ # Type-agnostic: valid as any supported type
847
857
  validates :ref, sec_id: true
848
858
  ```
849
859
 
@@ -862,16 +872,16 @@ With `normalize: true` in allowlist or agnostic mode, a value valid as more than
862
872
 
863
873
  ### Error messages and `details:`
864
874
 
865
- On failure the validator adds one error under the `:sec_id` key with a type-aware default ("is not a valid ISIN" for a single type, "is not a valid securities identifier" for an allowlist/agnostic). Override the message in either of two ways: pass the standard `message:` option (the simplest, per-validation override), or define the attribute-scoped i18n key `activemodel.errors.models.<model>.attributes.<attribute>.sec_id` in your locale files. (The generic `activemodel.errors.messages.sec_id` key is not consulted, because the built-in default is supplied as ActiveModel's `message:` fallback.) Pass `details: true` (with a single `type:` it is ignored for an allowlist/agnostic) to surface sec_id's specific reason instead of the generic text:
875
+ On failure the validator adds one error under the `:sec_id` key with a type-aware default ("is not a valid ISIN" for a single type, "is not a valid securities identifier" for an allowlist/agnostic). Override the message in either of two ways: pass the standard `message:` option (the simplest, per-validation override), or define the attribute-scoped i18n key `activemodel.errors.models.<model>.attributes.<attribute>.sec_id` in your locale files. (The generic `activemodel.errors.messages.sec_id` key is not consulted, because the built-in default is supplied as ActiveModel's `message:` fallback.) Pass `details: true` (with a single `type:`; it is ignored for an allowlist/agnostic) to surface sec_id's specific reason instead of the generic text:
866
876
 
867
877
  ```ruby
868
878
  validates :isin, sec_id: { type: :isin, details: true }
869
879
  # a bad checksum reports e.g. "Checksum '4' is invalid, expected '5'"
870
880
  ```
871
881
 
872
- Standard `EachValidator` options `allow_nil`, `allow_blank`, `if`, `unless`, `on` work as usual. Tested against Rails 7.2, 8.0, and 8.1.
882
+ Standard `EachValidator` options (`allow_nil`, `allow_blank`, `if`, `unless`, `on`) work as usual. Tested against Rails 7.2, 8.0, and 8.1.
873
883
 
874
- ## Lookup Service Integration
884
+ ## Lookup service integration
875
885
 
876
886
  SecID validates identifiers but does not include HTTP clients. The [`docs/guides/`](docs/guides/) directory provides integration patterns for external lookup services using only stdlib (`net/http`, `json`):
877
887
 
@@ -884,10 +894,10 @@ SecID validates identifiers but does not include HTTP clients. The [`docs/guides
884
894
 
885
895
  Each guide includes a complete adapter class and a [runnable example](examples/).
886
896
 
887
- ## Type Signatures (RBS)
897
+ ## Type signatures (RBS)
888
898
 
889
899
  sec_id ships hand-written [RBS](https://github.com/ruby/rbs) signatures under `sig/`,
890
- packaged in the gem so if you use [Steep](https://github.com/soutaro/steep) or an
900
+ packaged in the gem, so if you use [Steep](https://github.com/soutaro/steep) or an
891
901
  RBS-aware editor, sec_id's types resolve automatically on install, no `rbs collection`
892
902
  entry required. The only standard-library signature referenced is `date`, declared in
893
903
  `sig/manifest.yaml`.
@@ -913,10 +923,19 @@ committed signatures fall out of sync with the tables.
913
923
  ## Development
914
924
 
915
925
  After checking out the repo, run `bin/setup` to install dependencies.
916
- Then, run `bundle exec rake` to run the tests. You can also run `bin/console`
917
- for an interactive prompt that will allow you to experiment.
926
+ Then run `bundle exec rake` to run RuboCop, validate the RBS signatures, and run the specs.
927
+ `bin/console` gives you an interactive prompt to experiment in.
928
+
929
+ To use your working copy from another project, point its Gemfile at the checkout:
930
+ `gem 'sec_id', path: '/path/to/sec_id'`.
931
+
932
+ Releases are published from CI by pushing a tag, so there is no local publish task.
933
+
934
+ ## Support and status
935
+
936
+ Ask questions in [Discussions](https://github.com/svyatov/sec_id/discussions). Report bugs in the [issue tracker](https://github.com/svyatov/sec_id/issues). Both are public and searchable, so the next reader with your question finds the answer.
918
937
 
919
- To install this gem onto your local machine, run `bundle exec rake install`.
938
+ One person actively maintains SecID. They read bug reports and pull requests, and they track each identifier standard against its published specification. No response time is promised. See [Governance](CONTRIBUTING.md#governance) for who decides and what happens if they stop.
920
939
 
921
940
  ## Contributing
922
941
 
@@ -930,7 +949,21 @@ See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes, following [K
930
949
 
931
950
  ## Versioning
932
951
 
933
- This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html)
952
+ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
953
+
954
+ The public API that version promise covers is everything documented in this README and in the [API docs](https://rubydoc.info/gems/sec_id), except anything marked `@api private` in YARD. Concretely, it covers:
955
+
956
+ - the `SecID` module methods: `[]`, `identifiers`, `valid?`, `detect`, `parse`, `parse!`, `extract`, `scan`, `explain`, `generate`, and `suggest`
957
+ - every identifier class and its documented instance and class methods
958
+ - the `SecID::Errors`, `SecID::Suggestion`, `SecID::Scanner::Match`, and `SecID::CFI::Classification` value objects, and the exception hierarchy under `SecID::Error`
959
+ - the `sec_id` ActiveModel validator and its options
960
+ - the RBS signatures under `sig/`
961
+
962
+ Marked `@api private` and outside the promise: `SecID::Detector`, `SecID::Scanner` itself, `SecID::CFI::Tables`, `SecID::IBAN::CountryRules`, `SecID::BIC::CountryCodes`, `Base.detection_priority`, and the internals of the four concerns. These can change in any release.
963
+
964
+ Raising the minimum Ruby version is a breaking change and ships in a MAJOR release.
965
+
966
+ Removals follow a deprecation window; see [Deprecation policy](CONTRIBUTING.md#deprecation-policy).
934
967
 
935
968
  ## License
936
969
 
@@ -5,11 +5,11 @@ require 'sec_id'
5
5
  begin
6
6
  require 'active_model'
7
7
  rescue LoadError => e
8
- # :nocov: ActiveModel is always loadable in the test suite
8
+ # simplecov:disable ActiveModel is always loadable in the test suite
9
9
  raise LoadError, 'sec_id/active_model requires ActiveModel, which could not be loaded. Add ' \
10
10
  '`gem "activemodel"` to your Gemfile (or use this inside a Rails app) before ' \
11
11
  "requiring 'sec_id/active_model'. (#{e.message})"
12
- # :nocov:
12
+ # simplecov:enable
13
13
  end
14
14
 
15
15
  # ActiveModel validator for securities identifiers, registered under the `sec_id` key.
@@ -115,9 +115,10 @@ class SecIdValidator < ActiveModel::EachValidator
115
115
  # @return [String, nil] the message of the SecID::Error raised when normalizing `value` as `type`
116
116
  def capture_reason(type, value)
117
117
  SecID[type].normalize(value)
118
- # :nocov: unreachable — only called after normalize already raised in valid_value?, kept as a safe fallback
118
+ # simplecov:disable unreachable — only called after normalize already raised in valid_value?,
119
+ # kept as a safe fallback
119
120
  nil
120
- # :nocov:
121
+ # simplecov:enable
121
122
  rescue SecID::Error => e
122
123
  e.message
123
124
  end
@@ -56,6 +56,10 @@ module SecID
56
56
  '*' => 36, '@' => 37, '#' => 38
57
57
  }.freeze
58
58
 
59
+ # Extends the including identifier class with the concern's class methods and checksum reader.
60
+ #
61
+ # @param base [Class] the identifier class including this concern
62
+ # @return [void]
59
63
  # @api private
60
64
  def self.included(base)
61
65
  base.attr_reader :checksum
@@ -28,6 +28,10 @@ module SecID
28
28
  # Alphanumeric characters (digits then uppercase letters).
29
29
  ALPHANUMERIC = (DIGITS + ALPHA).freeze
30
30
 
31
+ # Extends the including identifier class with the concern's class methods.
32
+ #
33
+ # @param base [Class] the identifier class including this concern
34
+ # @return [void]
31
35
  # @api private
32
36
  def self.included(base)
33
37
  base.extend(ClassMethods)
@@ -8,6 +8,10 @@ module SecID
8
8
  # Characters stripped during normalization (whitespace and hyphens); classes may override.
9
9
  SEPARATORS = /[\s-]/
10
10
 
11
+ # Extends the including identifier class with the concern's class methods.
12
+ #
13
+ # @param base [Class] the identifier class including this concern
14
+ # @return [void]
11
15
  # @api private
12
16
  def self.included(base)
13
17
  base.extend(ClassMethods)
@@ -7,7 +7,8 @@ module SecID
7
7
  #
8
8
  # Included by the 9 checksum types. The candidate space is the human-error net —
9
9
  # {HOMOGLYPHS} plus adjacent transpositions — never a full coincidental net. `valid?`
10
- # is the oracle, so no false candidate ever escapes; the {HOMOGLYPHS} table bounds recall.
10
+ # is the oracle, so no checksum-invalid candidate ever escapes (a valid candidate is not
11
+ # necessarily the intended correction); the {HOMOGLYPHS} table bounds recall.
11
12
  #
12
13
  # @see SecID::Suggestion
13
14
  #
@@ -30,6 +31,10 @@ module SecID
30
31
  # then the checksum-recompute fallback last (R6/R7).
31
32
  RANK = { substitution: 0, transposition: 1, checksum: 2 }.freeze
32
33
 
34
+ # Extends the including identifier class with the concern's class methods.
35
+ #
36
+ # @param base [Class] the identifier class including this concern
37
+ # @return [void]
33
38
  # @api private
34
39
  def self.included(base)
35
40
  base.extend(ClassMethods)
@@ -18,6 +18,10 @@ module SecID
18
18
  invalid_country: InvalidStructureError
19
19
  }.freeze
20
20
 
21
+ # Extends the including identifier class with the concern's class methods.
22
+ #
23
+ # @param base [Class] the identifier class including this concern
24
+ # @return [void]
21
25
  # @api private
22
26
  def self.included(base)
23
27
  base.extend(ClassMethods)
@@ -46,11 +46,15 @@ module SecID
46
46
  # The non-nil confidence tiers `confidence` can take; `:checksum` candidates carry `nil`.
47
47
  CONFIDENCE_LEVELS = %i[high medium].freeze
48
48
 
49
+ # Returns a JSON-compatible hash representation of the suggestion.
50
+ #
49
51
  # @return [Hash] the symbol-keyed field hash (JSON-compatible)
50
52
  def as_json(*)
51
53
  to_h
52
54
  end
53
55
 
56
+ # Returns the corrected identifier as a string.
57
+ #
54
58
  # @return [String] the corrected identifier string
55
59
  def to_s
56
60
  identifier.to_s
@@ -2,5 +2,5 @@
2
2
 
3
3
  module SecID
4
4
  # Current gem version.
5
- VERSION = '7.1.0'
5
+ VERSION = '7.1.1'
6
6
  end
data/lib/sec_id.rb CHANGED
@@ -158,7 +158,9 @@ module SecID
158
158
  input = str.to_s.strip.upcase
159
159
  suggestable_types(input, types)
160
160
  .flat_map { |klass| klass.suggest(input) }
161
- .sort_by { |suggestion| [Suggestable::RANK.fetch(suggestion.edit), self[suggestion.type].detection_priority.last] }
161
+ .sort_by do |suggestion|
162
+ [Suggestable::RANK.fetch(suggestion.edit), self[suggestion.type].detection_priority.last]
163
+ end
162
164
  end
163
165
 
164
166
  private
data/sec_id.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.authors = ['Leonid Svyatov']
11
11
  spec.email = ['leonid@svyatov.ru']
12
12
 
13
- spec.summary = 'A Ruby toolkit for securities identifiers validate, parse, normalize, detect, convert, ' \
13
+ spec.summary = 'A Ruby toolkit for securities identifiers: validate, parse, normalize, detect, convert, ' \
14
14
  'generate, classify, and repair.'
15
15
  spec.description = 'Validate, normalize, parse, convert, generate, classify, and repair securities identifiers. ' \
16
16
  'Auto-detect identifier type from any string. Calculate and restore checksums. Suggest ' \
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sec_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.0
4
+ version: 7.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
@@ -126,8 +126,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
126
  - !ruby/object:Gem::Version
127
127
  version: '0'
128
128
  requirements: []
129
- rubygems_version: 4.0.12
129
+ rubygems_version: 4.0.16
130
130
  specification_version: 4
131
- summary: A Ruby toolkit for securities identifiers validate, parse, normalize, detect,
132
- convert, generate, classify, and repair.
131
+ summary: 'A Ruby toolkit for securities identifiers: validate, parse, normalize, detect,
132
+ convert, generate, classify, and repair.'
133
133
  test_files: []