Skip to content

Releases: svyatov/sec_id

v7.1.1

Choose a tag to compare

@svyatov svyatov released this 31 Jul 14:55
v7.1.1
b9d9784

Documentation and packaging only. No identifier, checksum, or public method changed.

This is the first release published from CI through RubyGems trusted publishing, signed with sigstore and pushed with a build provenance attestation.

Changed

  • 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
  • CHANGELOG version headings resolve to comparison links, and 2.0.1 has the entry it never had
  • Gem summary punctuation matches the README tagline

Install

gem 'sec_id', '~> 7.1'

Verify

curl -s https://rubygems.org/api/v1/attestations/sec_id-7.1.1.json
git tag -v v7.1.1

Full changelog: https://github.com/svyatov/sec_id/blob/main/CHANGELOG.md#711---2026-07-31

v7.1.0

Choose a tag to compare

@svyatov svyatov released this 15 Jul 08:16
16ff867

Added

  • 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

Install

gem 'sec_id', '~> 7.1'

Full changelog: https://github.com/svyatov/sec_id/blob/main/CHANGELOG.md#710---2026-07-15

v7.0.0

Choose a tag to compare

@svyatov svyatov released this 14 Jul 12:55
6909810

sec_id 7.0.0 adds UPI (ISO 4914) support and renames the check-digit concept to checksum across the entire public API. The rename ships with a v7 deprecation bridge that keeps every old name working through v7 (removed in v8) — only the :invalid_check_digit → :invalid_checksum validation error code flips immediately, with no bridge. Upgrading from 6.x? See the migration guide.

Added

  • 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.

Changed

  • 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

Deprecated

  • 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

Full changelog: https://github.com/svyatov/sec_id/blob/v7.0.0/CHANGELOG.md

v6.1.0

Choose a tag to compare

@svyatov svyatov released this 10 Jul 10:08
60bfda7

Minor release. Two additive class-level/instance additions, no breaking changes — a drop-in upgrade from 6.0.0.

Added

  • 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.

    case SecID.parse('US0378331005')
    in { country_code: 'US', nsin: }
      puts "US security, NSIN #{nsin}"
    end
  • 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) and 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.

Full Changelog: v6.0.0...v6.1.0

v6.0.0

Choose a tag to compare

@svyatov svyatov released this 08 Jul 15:39
6123227

Major release. See MIGRATION.md for upgrading from 5.x — every breaking change is confined to SecID::CFI.

Added

  • Hand-written RBS type signatures under sig/, shipped in the gem — consumers running Steep or an RBS-aware editor resolve sec_id's types on install. Core library checked by Steep in strict mode (zero errors) and verified at runtime via RBS::Test. Zero runtime dependency preserved.
  • SecID::CFI#decode returns a frozen Classification value object for any valid CFI — Field objects carrying the CFI letter (#code), semantic symbol (#name), ISO 10962 label (#label), and domain-scoped <name>? predicates (decode.category.equity?, decode.attributes.voting_right.voting?). Ships #to_s/#to_h/#as_json.
  • SecID::CFI full ISO 10962:2021 attribute validation — positions 3–6 checked per-group, Strategies (K) require XXXX, pure-N/A positions accept only X, and the ED cross-position rule is enforced. New :invalid_attribute error code.
  • Opt-in ActiveModel / Rails validator (require 'sec_id/active_model'), registered as sec_id: validates :isin, sec_id: { type: :isin }. Single type, allowlist, or any type; opt-in normalize: and details:. Auto-activates in Rails via a Railtie and adds no runtime dependency. Tested across Rails 7.2, 8.0, 8.1, and main.
  • BIC / SWIFT code (ISO 9362) support via SecID::BIC — validate, normalize, parse, detect, extract, generate 8-/11-char codes with bank_code, country_code, location_code, branch_code. Country code validated against a frozen ISO 3166-1 / SWIFT set.
  • DTI (ISO 24165, Digital Token Identifier) support via SecID::DTI — the 15th identifier type and the first offline DTI validator in any language. ISO 7064 hybrid MOD 31,30 over the 30-symbol DTI alphabet; Bitcoin's hand-assigned code honored via an exception map. check_digit returns a String.
  • .generate on all 15 types plus SecID.generate(:type) — syntactically valid identifiers (with correct check digits) as test fixtures; accepts a random: keyword for reproducibility. Values are format-valid only, not real securities.
  • 100% YARD documentation coverage of the public API, enforced in CI via rake yard:stats. Ships .yardopts and a documentation_uri (Documentation link on RubyGems).

Changed

  • Internal IBAN country-data module dissolved into SecID::IBAN — SecID::IBAN::COUNTRY_RULES / LENGTH_ONLY_COUNTRIES (was SecID::IBANCountryRules::*).
  • SecID.valid? and SecID.parse are faster and allocate less — short-circuit on first match / reuse the detected instance.
  • BREAKING: SecID::CFI.valid? is now strict at the attribute level for all 14 categories — codes with attribute letters outside the ISO 10962:2021 tables are now invalid (e.g. CFI.valid?('ESZZZZ') → false). No leniency option. The ActiveModel validator inherits this.
  • BREAKING: SecID::CFI group tables corrected to ISO 10962:2021 — six categories carried wrong group letters (non-listed options H classified by underlying; D/L/T corrected; phantom FM/IM/JM/LM removed; symbols renamed, e.g. LS → :securities_lending, TI → :indices).

Removed

  • BREAKING: the 12 hardcoded equity predicate helpers on SecID::CFI (equity?, voting?, fully_paid?, bearer?, …). Migrate to cfi.decode and its scoped field predicates — cfi.voting? → cfi.decode.attributes.voting_right.voting?; cfi.equity? → cfi.decode.category.equity?; cfi.no_restrictions? → cfi.decode.attributes.ownership_restrictions.free_of_restrictions?.

v5.2.0

Choose a tag to compare

@svyatov svyatov released this 24 Feb 17:16
da19fe2

Added

  • 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)
  • SecID.explain method for debugging identifier detection — returns per-type validation results showing exactly why each type matched or rejected the input
  • on_ambiguous: option for SecID.parse and SecID.parse! — :first (default, existing behavior), :raise (raises AmbiguousMatchError), :all (returns array of all matching instances)
  • SecID::AmbiguousMatchError exception class for ambiguous identifier detection
  • #as_json method on all identifier types (delegates to #to_h) and on Errors (delegates to #details) for JSON serialization compatibility
  • SecID::IBAN.supported_countries class method returning sorted array of all supported country codes
  • SecID::CFI.categories class method returning the categories hash
  • SecID::CFI.groups_for(category_code) class method returning groups hash for a given category

v5.1.0

Choose a tag to compare

@svyatov svyatov released this 19 Feb 17:45
5da9bf0

Added

  • #==, #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
  • #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)
  • #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)
  • Lookup service integration guides and runnable examples for OpenFIGI, SEC EDGAR, GLEIF, and Eurex APIs (docs/guides/, examples/)
  • GitHub community standards files: Code of Conduct, Contributing guide, Security policy, issue templates, and PR template

Full Changelog: v5.0.0...v5.1.0

v5.0.0

Choose a tag to compare

@svyatov svyatov released this 17 Feb 16:02
688ebbd

Added

  • Validatable, Normalizable, and IdentifierMetadata concerns extracted from Base, making each responsibility independently includable
  • #validate and .validate methods on all identifier types that eagerly trigger + cache errors and return self/instance
  • #validate! and .validate! methods that raise InvalidFormatError, InvalidCheckDigitError, or InvalidStructureError on validation failure, returning self/instance on success
  • Rails-like #errors API returning Errors with details, messages, none?, any?, empty?, size, each, and to_a on all identifier classes, with type-specific error detection for check digits, FIGI prefixes, CFI categories/groups, IBAN BBAN format, and OCC dates
  • #restore instance method on check-digit identifiers returning the full identifier string without mutation
  • .restore class method on check-digit identifiers returning the full identifier string
  • SecID.parse(str, types: nil) and SecID.parse!(str, types: nil) methods that return a typed identifier instance for the most specific match, with optional type filtering
  • SecID.valid?(str, types: nil) method for quick boolean validation against all or specific identifier types
  • SecID.detect(str) method that identifies all matching identifier types for a given string, returning symbols sorted by specificity
  • Metadata registry: SecID.identifiers returns all identifier classes, SecID[:isin] looks up by symbol key
  • Metadata class methods on all identifiers: short_name, full_name, id_length, example, has_check_digit?
  • #normalized and #normalize instance methods on all identifier types returning the canonical string form
  • #normalize! instance method on all identifier types that mutates full_id to canonical form and returns self
  • .normalize(id) class method on all identifier types that strips separators, upcases, validates, and returns the canonical string
  • SEPARATORS constant in Normalizable (/[\s-]/) included in Base, with type-specific overrides for OCC and FISN (/-/)

Changed

  • BREAKING: Minimum Ruby version raised from 3.1 to 3.2 (Ruby 3.1 reached EOL on 2025-03-31)
  • BREAKING: #restore! now returns self instead of a string; use #restore for the string return value
  • BREAKING: .restore! now returns the restored instance instead of a string; use .restore for the string return value
  • BREAKING: #normalize! on CIK, OCC, and Valoren now returns self instead of a string; use #normalized to get the canonical string
  • BREAKING: Class-level .normalize! on CIK, OCC, and Valoren replaced by .normalize (non-bang) which returns the canonical string
  • BREAKING: Base#parse always upcases input; the upcase keyword parameter is removed
  • BREAKING: #full_number renamed to #full_id on all identifier types
  • BREAKING: Ruby module renamed from SecId to SecID (e.g. SecId::ISIN → SecID::ISIN)
  • Luhn helper methods in Checkable are now private (implementation detail)

Removed

  • Class-level .normalize! on CIK, OCC, and Valoren — replaced by .normalize
  • upcase keyword parameter from Base#parse
  • #valid_format? instance method (now private) and .valid_format? class method
  • OCC#full_symbol method — use #full_id instead

Fixed

  • 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
  • OCC #date memoization for invalid dates — previously re-attempted parsing on every call instead of caching nil
  • LEI restore and to_s now correctly pad single-digit check digits to 2 characters
  • Valoren#to_isin no longer mutates the source instance

v4.4.1

Choose a tag to compare

@svyatov svyatov released this 05 Feb 18:48
5aef8f7

Fixed

  • CUSIP#to_isin and SEDOL#to_isin no longer mutate source instance when check digit is missing (#127)

v4.4.0

Choose a tag to compare

@svyatov svyatov released this 29 Jan 18:37
611f398

What's New

This release adds 5 new identifier types and cross-identifier conversions, bringing the total to 13 supported securities identifiers.

New Identifier Types

  • WKN - Wertpapierkennnummer (German securities identifier)
  • Valoren - Swiss Security Number
  • CFI - Classification of Financial Instruments (ISO 10962) with category/group validation and equity-specific predicates
  • FISN - Financial Instrument Short Name (ISO 18774)
  • CEI - CUSIP Entity Identifier for syndicated loan market

Cross-Identifier Conversions

Convert between regional identifiers and ISIN:

# Regional → ISIN
SecId::SEDOL.new('B0WNLY7').to_isin           # => "GB00B0WNLY75"
SecId::WKN.new('A0Q4DC').to_isin              # => "DE000A0Q4DC4"
SecId::Valoren.new('1203204').to_isin         # => "CH0012032048"

# ISIN → Regional
SecId::ISIN.new('GB00B0WNLY75').to_sedol      # => "B0WNLY7"
SecId::ISIN.new('DE000A0Q4DC4').to_wkn        # => "A0Q4DC"
SecId::ISIN.new('CH0012032048').to_valoren    # => "1203204"

New nsin_type and to_nsin methods for country-aware NSIN extraction.

Bug Fixes

  • Allow Crown Dependencies (GG, IM, JE) and Overseas Territories (FK) in SEDOL/ISIN conversions
  • Removed BR (Brazil) from CGS country codes — Brazil never used CINS numbers

Internal Improvements

  • Extracted shared Checkable concern consolidating all check-digit logic
  • Performance optimization for hot paths
  • Simplified class hierarchy

Full Changelog: v4.3.0...v4.4.0