Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog. This project adheres to Semantic Versioning and Conventional Commits.

[Unreleased]

[6.0.0] - 2026-07-08

Added

  • 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
  • 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
  • SecID::CFI now validates the full ISO 10962:2021 attribute matrix — positions 3–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
  • 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
  • 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
  • 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
  • .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
  • 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

Changed

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

Removed

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

[5.2.0] - 2026-02-24

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

[5.1.0] - 2026-02-19

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

[5.0.0] - 2026-02-17

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::ISINSecID::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

[4.4.1] - 2026-02-05

Fixed

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

[4.4.0] - 2026-01-29

Added

  • Cross-identifier conversions: SEDOL, WKN, and Valoren to_isin methods with country code validation; ISIN to_sedol, to_wkn, to_valoren methods with predicate helpers (sedol?, wkn?, valoren?) (#115)
  • ISIN nsin_type and to_nsin methods for country-aware NSIN extraction (#114)
  • CEI (CUSIP Entity Identifier) support for syndicated loan market entity identification (#113)
  • FISN (Financial Instrument Short Name) support per ISO 18774 (#112)
  • CFI (Classification of Financial Instruments) support with category/group validation and equity-specific predicates (#111)
  • Valoren support (Swiss Security Number) (@wtn, #109)
  • WKN support (Wertpapierkennnummer - German securities identifier) (@wtn, #108)

Changed

  • Replaced has_check_digit DSL with explicit Checkable concern that consolidates all check-digit logic (constants, Luhn algorithms, validation, restoration)
  • Simplified Base class to core validation and parsing; check-digit classes now include Checkable
  • Non-check-digit classes (CIK, OCC, WKN, Valoren, CFI, FISN) no longer need any special declaration
  • Moved Normalizable module to lib/sec_id/concerns/ for consistency with other concerns
  • Optimized hot paths by replacing &method(:char_to_digit) with inline blocks to avoid Method object allocation
  • Added frozen Set constants for ISIN country code lookups (SEDOL_COUNTRY_CODES, VALOREN_COUNTRY_CODES)

Fixed

  • Allow Crown Dependencies (GG, IM, JE) and Overseas Territories (FK) in SEDOL/ISIN conversions (@wtn, #117)
  • Removed BR (Brazil) from CGS country codes — Brazil never used CINS numbers and Brazilian ISINs cannot be converted to CUSIP (@wtn, #110)

[4.3.0] - 2025-01-13

Added

  • LEI support (Legal Entity Identifier, ISO 17442)
  • IBAN support (International Bank Account Number, ISO 13616) with EU/EEA country validation

Changed

  • Improved README: better formatting, navigation, and clear API distinction between check-digit and normalization identifiers
  • Refactored CIK and OCC to inherit from Base class with has_check_digit? hook for cleaner architecture
  • Added Normalizable module for consistent normalize! class method across identifiers
  • Added validate_format_for_calculation! helper method to Base class to reduce code duplication
  • Added comprehensive YARD documentation to all classes (public and private methods)
  • Applied Stepdown Rule to method ordering throughout codebase
  • Created shared RSpec examples for edge cases (nil, empty, whitespace inputs)
  • Created shared RSpec examples for check-digit and normalization identifiers
  • Applied shared examples to all identifier specs, removing ~350 lines of duplicate test code
  • Improved test maintainability with 582 tests covering all identifier types

[4.2.0] - 2025-01-12

Added

Changed

  • Separate CIK from Base for cleaner architecture (@wtn, #92)
  • Use rubocop-rspec plugin (@wtn, #90)
  • Replace CodeClimate with Codecov
  • Add permissions to CI workflow
  • Clean up gemspec: update description and simplify files list

Fixed

  • CUSIP#cins? usage example in README (@wtn, #91)

[4.1.0] - 2024-09-23

Added

Changed

  • Small internal refactorings

[4.0.0] - 2024-07-09

Changed

  • BREAKING: Minimum required Ruby version is 3.1 now
  • BREAKING: Default repository branch renamed to main
  • Small internal refactorings
  • TravisCI -> GitHub Actions
  • Dropped tests for Ruby below 3.1
  • Rubocop's Ruby target version changed to 3.1

[3.0.0] - 2020-03-10

Changed

  • BREAKING: Minimum required Ruby version is 2.5 now
  • Small internal refactorings
  • TravisCI config updated: dropped Ruby 2.3 and 2.4, added Ruby 2.7
  • Rubocop's Ruby target version changed to 2.5

[2.0.0] - 2019-02-03

Added

  • SEDOL numbers support: SecID::SEDOL

Changed

  • BREAKING: API for accessing full number is unified across all classes:

    ```
    SecId::ISIN#full_number  # previously SecId::ISIN#isin
    SecId::CUSIP#full_number # previously SecId::CUSIP#cusip
    SecId::SEDOL#full_number
    ```
    

Fixed

  • CUSIP check-digit algorithm fixed

[1.1.0] - 2019-02-03

Added

  • CUSIP numbers support: SecID::CUSIP
  • CHANGELOG.md file added

Changed

  • Char to digit conversion now uses precalculated tables instead of dynamic calculation for speed

[1.0.0] - 2017-10-25

Added

  • ISIN numbers support: SecID::ISIN