Releases: svyatov/sec_id
Release list
v7.1.1
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 privateinternals 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.1Full changelog: https://github.com/svyatov/sec_id/blob/main/CHANGELOG.md#711---2026-07-31
v7.1.0
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 such asO↔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-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
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
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 (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
Full changelog: https://github.com/svyatov/sec_id/blob/v7.0.0/CHANGELOG.md
v6.1.0
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 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 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 existingshort_name/full_name/id_lengthmetadata 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
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#decodereturns a frozenClassificationvalue object for any valid CFI —Fieldobjects 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::CFIfull ISO 10962:2021 attribute validation — positions 3–6 checked per-group,Strategies(K) requireXXXX, pure-N/A positions accept onlyX, and theEDcross-position rule is enforced. New:invalid_attributeerror code.- Opt-in ActiveModel / Rails validator (
require 'sec_id/active_model'), registered assec_id:validates :isin, sec_id: { type: :isin }. Single type, allowlist, or any type; opt-innormalize:anddetails:. 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 withbank_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_digitreturns aString. .generateon all 15 types plusSecID.generate(:type)— syntactically valid identifiers (with correct check digits) as test fixtures; accepts arandom: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.yardoptsand adocumentation_uri(Documentation link on RubyGems).
Changed
- Internal IBAN country-data module dissolved into
SecID::IBAN—SecID::IBAN::COUNTRY_RULES/LENGTH_ONLY_COUNTRIES(wasSecID::IBANCountryRules::*). SecID.valid?andSecID.parseare 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::CFIgroup tables corrected to ISO 10962:2021 — six categories carried wrong group letters (non-listed optionsHclassified by underlying;D/L/Tcorrected; phantomFM/IM/JM/LMremoved; 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 tocfi.decodeand 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
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
v5.1.0
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
Full Changelog: v5.0.0...v5.1.0
v5.0.0
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
v4.4.1
v4.4.0
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
Checkableconcern consolidating all check-digit logic - Performance optimization for hot paths
- Simplified class hierarchy
Full Changelog: v4.3.0...v4.4.0