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 (rbsandsteep,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), andrake sig:cfi(regenerate the CFI dynamic-method signatures fromSecID::CFI::Tables, guarded by a drift spec). CI gains atypesjob on Ruby 4.0. The optional ActiveModel adapter and Railtie are excluded from the typed scope SecID::CFI#decodereturns a frozenSecID::CFI::Classificationvalue object for any valid CFI (nilfor invalid). Its category, group, and each attribute areFieldobjects carrying the CFI letter (#code), semantic symbol (#name), and authoritative ISO 10962 label (#label), plus<name>?predicates scoped to the field's own domain — sodecode.category.equity?anddecode.attributes.voting_right.voting?answer, while an out-of-domain predicate (e.g.category.voting?) raisesNoMethodError.#attributesis anEnumerableof the attribute fields keyed by each position's group meaning (attributes.voting_right, nil-safeattributes[:form]). Ships a human-readable#to_sand#to_h/#as_jsonfor serialization.Xdecodes to:not_applicable; pure-N/A positions are omittedSecID::CFInow validates the full ISO 10962:2021 attribute matrix — positions 3–6 are checked per-group,Strategies(K) codes requireXXXX, pure-N/A positions accept onlyX, and theED(depositary receipts on equities) cross-position rule is enforced. A new:invalid_attributeerror code (mapped toInvalidStructureError) names the offending positions and group- Opt-in ActiveModel / Rails validator (
require 'sec_id/active_model'), registered assec_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 raisesArgumentErrorat class-load. Opt-innormalize: trueaccepts separatored input and rewrites the attribute to canonical form on success; opt-indetails: truesurfaces the specific failure reason. Auto-activates in Rails via a Railtie (norequire:/initializer needed) and adds no runtime dependency — nothing on the defaultrequire '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. Exposesbank_code,country_code,location_code, andbranch_code(nilfor a BIC8) components. Validates the embedded country code against a frozen ISO 3166-1 / SWIFT-recognized set (SecID::BIC.countries), raisingInvalidStructureError(: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 andYnever 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 byvalid?,restore, andcheck_digitalike. Unlike every other check-digit type in the gem,check_digit/calculate_check_digitreturn aStringrather than anInteger, since DTI check characters can be letters .generateon all 15 identifier types plus a centralSecID.generate(:type)dispatcher for producing syntactically valid identifiers (with correct check digits where applicable) as test fixtures — accepts an optionalrandom:keyword (a RubyRandom) 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.yardoptssorubydoc.inforenders the public API cleanly (private internals hidden, README/CHANGELOG/LICENSE as pages) and exposesdocumentation_urimetadata (a Documentation link on the RubyGems page). Adds ayarddev/test dependency (require: false— the gem stays zero-runtime-dependency) and arake yardtask for local HTML docs
Changed
- Internal
@api privateIBAN country-data module dissolved into theSecID::IBANclass, so constant names match their file paths per Ruby convention:SecID::IBANCountryRules::COUNTRY_RULES/LENGTH_ONLY_COUNTRIESare nowSecID::IBAN::COUNTRY_RULES/SecID::IBAN::LENGTH_ONLY_COUNTRIES SecID.valid?andSecID.parseare faster and allocate less —SecID.valid?short-circuits on the first matching type instead of running a full detect-and-sort, andSecID.parsereuses 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 categoriesS,H,J) — codes with attribute letters outside the ISO 10962:2021 tables, previously accepted, are now invalid (e.g.CFI.valid?('ESZZZZ')returnsfalse). There is no leniency option, matching every other identifier type in the gem. The ActiveModel validator inherits this strictness automatically - BREAKING:
SecID::CFIgroup tables corrected to ISO 10962:2021 — six categories carried wrong group letters. Non-listed options (H) are now classified by underlying (HRRates,HTCommodities,HEEquity,HCCredit,HFFX,HMOthers) instead of the copied listed-option call/put shape;DgainsDE(structured products without capital protection) and correctsDG/DA/DN(dropping the inventedmunicipal_notes);LbecomesLL/LR/LS,TbecomesTC/TT/TR/TI/…, and the phantomFM/IM/JM/LMgroups are removed. Group symbols renamed accordingly (e.g.LS→:securities_lending,TI→:indices,MM→:other_assets). RealH-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: usecfi.decodeand 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: migratecfi.equity?tocfi.decode.category.equity?(orcfi.category == :equity, unchanged), andcfi.no_restrictions?tocfi.decode.attributes.ownership_restrictions.free_of_restrictions?(the ISO value name)
[5.2.0] - 2026-02-24
Added
SecID.scanandSecID.extractmethods for finding identifiers in freeform text — returnsScanner::Matchobjects (Data.define(:type, :raw, :range, :identifier)) with the validated identifier instance; supportstypes:filtering, hyphenated identifiers, and compound patterns (OCC with spaces, FISN with slashes)SecID.explainmethod for debugging identifier detection — returns per-type validation results showing exactly why each type matched or rejected the inputon_ambiguous:option forSecID.parseandSecID.parse!—:first(default, existing behavior),:raise(raisesAmbiguousMatchError),:all(returns array of all matching instances)SecID::AmbiguousMatchErrorexception class for ambiguous identifier detection#as_jsonmethod on all identifier types (delegates to#to_h) and onErrors(delegates to#details) for JSON serialization compatibilitySecID::IBAN.supported_countriesclass method returning sorted array of all supported country codesSecID::CFI.categoriesclass method returning the categories hashSecID::CFI.groups_for(category_code)class method returning groups hash for a given category
[5.1.0] - 2026-02-19
Added
#==,#eql?, and#hashmethods 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_hmethod 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_sand.to_pretty_sdisplay formatting methods on all identifier types, returning a human-readable string ornilfor 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, andIdentifierMetadataconcerns extracted fromBase, making each responsibility independently includable#validateand.validatemethods on all identifier types that eagerly trigger + cache errors and returnself/instance#validate!and.validate!methods that raiseInvalidFormatError,InvalidCheckDigitError, orInvalidStructureErroron validation failure, returning self/instance on success- Rails-like
#errorsAPI returningErrorswithdetails,messages,none?,any?,empty?,size,each, andto_aon all identifier classes, with type-specific error detection for check digits, FIGI prefixes, CFI categories/groups, IBAN BBAN format, and OCC dates #restoreinstance method on check-digit identifiers returning the full identifier string without mutation.restoreclass method on check-digit identifiers returning the full identifier stringSecID.parse(str, types: nil)andSecID.parse!(str, types: nil)methods that return a typed identifier instance for the most specific match, with optional type filteringSecID.valid?(str, types: nil)method for quick boolean validation against all or specific identifier typesSecID.detect(str)method that identifies all matching identifier types for a given string, returning symbols sorted by specificity- Metadata registry:
SecID.identifiersreturns 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? #normalizedand#normalizeinstance methods on all identifier types returning the canonical string form#normalize!instance method on all identifier types that mutatesfull_idto canonical form and returnsself.normalize(id)class method on all identifier types that strips separators, upcases, validates, and returns the canonical stringSEPARATORSconstant inNormalizable(/[\s-]/) included inBase, 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 returnsselfinstead of a string; use#restorefor the string return value - BREAKING:
.restore!now returns the restored instance instead of a string; use.restorefor the string return value - BREAKING:
#normalize!on CIK, OCC, and Valoren now returnsselfinstead of a string; use#normalizedto 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#parsealways upcases input; theupcasekeyword parameter is removed - BREAKING:
#full_numberrenamed to#full_idon all identifier types - BREAKING: Ruby module renamed from
SecIdtoSecID(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 upcasekeyword parameter fromBase#parse#valid_format?instance method (now private) and.valid_format?class methodOCC#full_symbolmethod — use#full_idinstead
Fixed
to_strnow always returns the same value asto_sacross all identifier types — previously LEI, IBAN, and Checkable identifiers could return divergent strings due to Rubyaliasresolving to the parent class method- OCC
#datememoization for invalid dates — previously re-attempted parsing on every call instead of cachingnil - LEI
restoreandto_snow correctly pad single-digit check digits to 2 characters Valoren#to_isinno longer mutates the source instance
[4.4.1] - 2026-02-05
Fixed
CUSIP#to_isinandSEDOL#to_isinno 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_isinmethods with country code validation; ISINto_sedol,to_wkn,to_valorenmethods with predicate helpers (sedol?,wkn?,valoren?) (#115) - ISIN
nsin_typeandto_nsinmethods 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_digitDSL with explicitCheckableconcern that consolidates all check-digit logic (constants, Luhn algorithms, validation, restoration) - Simplified
Baseclass to core validation and parsing; check-digit classes nowinclude Checkable - Non-check-digit classes (CIK, OCC, WKN, Valoren, CFI, FISN) no longer need any special declaration
- Moved
Normalizablemodule tolib/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
Normalizablemodule for consistentnormalize!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
[4.1.0] - 2024-09-23
Added
- FIGI support (@wtn, #84)
- CIK support (@wtn, #85)
- Convert between CUSIPs and ISINs (@wtn, #86, #88)
- CINS check method for CUSIPs (@wtn, #87)
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