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]
[7.1.0] - 2026-07-15
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)andSecID.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-rankedSecID::Suggestionvalue 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 sharedSuggestableconcern. Candidates rank:high(homoglyph) then:medium(transposition) then a:checksumrecompute fallback last; there is no:lowtier (coincidental substitutions are never generated), keeping results small and high-precision.suggestnever 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.suggestsilently 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 anO-for-0/I-for-1typo), 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
[7.0.0] - 2026-07-14
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 (fixedQZprefix, 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 andYnever appear), pinned empirically against DSB-issued vectors — no DSB registry lookup or paywalled ISO 4914 spec required. Like DTI,checksum/calculate_checksumreturn aStringrather than anInteger, 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 methodscheck_digit→checksum,calculate_check_digit→calculate_checksum, andhas_check_digit?→has_checksum?; the error classSecID::InvalidCheckDigitError→SecID::InvalidChecksumError; the error code:invalid_check_digit→:invalid_checksum(a hard flip with no dual emission — dual would duplicateerrors.detailsentries, so update anyerrors.details/explain/ActiveModeldetails: truematcher immediately); and the:check_digitcomponents key →:checksuminto_h/deconstruct_keysacross all nine checkable types.restore/restore!and theCheckableconcern 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, theInvalidCheckDigitErrorconstant, and the:check_digitcomponents 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 viaKernel#warnon every call, visible at Ruby's default verbosity and silenceable with-W0/$VERBOSE = nilor an app-levelWarningoverride; theSecID::InvalidCheckDigitErrorconstant, a same-object alias ofInvalidChecksumErrorsorescueunder either name keeps working; and the:check_digitkey, still present alongside:checksumincomponents/to_h/deconstruct_keys
[6.1.0] - 2026-07-10
Added
SecID::Base#deconstruct_keys— every identifier now destructures incase/inpatterns, exposing the same parsed fields#to_hreports under:components. Thekeysargument is ignored, no new keys are introduced, and validity is not part of the protocol:SecID.parsereturningnilremains the validity guard, and an instance built from unparseable input bindsnilfor each component. Nodeconstruct(array-pattern) method is definedSecID::Base.type_key— the memoized class-level registry symbol for an identifier type, joining the existingshort_name/full_name/id_lengthmetadata 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
[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