From d3b160aa18b65c5155abbc2a623dcf372293d01d Mon Sep 17 00:00:00 2001 From: "William T. Nelson" <35801+wtn@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:21:57 -0500 Subject: [PATCH] CIK support added --- README.md | 23 ++++++- lib/sec_id.rb | 1 + lib/sec_id/cik.rb | 35 +++++++++++ spec/sec_id/cik_spec.rb | 129 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 lib/sec_id/cik.rb create mode 100644 spec/sec_id/cik_spec.rb diff --git a/README.md b/README.md index ffc25a2..80d7b81 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Currently supported standards: [ISIN](https://en.wikipedia.org/wiki/International_Securities_Identification_Number), [CUSIP](https://en.wikipedia.org/wiki/CUSIP), [SEDOL](https://en.wikipedia.org/wiki/SEDOL), -[FIGI](https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier). +[FIGI](https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier), [CIK](https://en.wikipedia.org/wiki/Central_Index_Key). Work in progress: [IBAN](https://en.wikipedia.org/wiki/International_Bank_Account_Number). @@ -179,6 +179,27 @@ figi.restore! # => 'BBG000DMBXR2' figi.calculate_check_digit # => 2 ``` +### SecId::CIK full example + +```ruby +# class level +SecId::CIK.valid?('0001094517') # => true +SecId::CIK.valid_format?('0001094517') # => true +SecId::CIK.restore!('1094517') # => '0001094517' +SecId::CIK.check_digit('0001094517') # raises NotImplementedError + +# instance level +cik = SecId::CIK.new('0001094517') +cik.full_number # => '0001094517' +cik.padding # => '000' +cik.identifier # => '1094517' +cik.valid? # => true +cik.valid_format? # => true +cik.restore! # => '0001094517' +cik.calculate_check_digit # raises NotImplementedError +cik.check_digit # => nil +``` + ## Development After checking out the repo, run `bin/setup` to install dependencies. diff --git a/lib/sec_id.rb b/lib/sec_id.rb index 5c70327..2c80a64 100644 --- a/lib/sec_id.rb +++ b/lib/sec_id.rb @@ -7,6 +7,7 @@ require 'sec_id/cusip' require 'sec_id/sedol' require 'sec_id/figi' +require 'sec_id/cik' module SecId Error = Class.new(StandardError) diff --git a/lib/sec_id/cik.rb b/lib/sec_id/cik.rb new file mode 100644 index 0000000..3946f51 --- /dev/null +++ b/lib/sec_id/cik.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module SecId + # https://en.wikipedia.org/wiki/Central_Index_Key + class CIK < Base + ID_REGEX = /\A + (?=\d{1,10}\z)(?0*)(?[1-9]\d{0,9}) + \z/x + + attr_reader :padding + + def initialize(cik) + cik_parts = parse cik + @padding = cik_parts[:padding] + @identifier = cik_parts[:identifier] + end + + def valid? + valid_format? + end + + def valid_format? + !identifier.nil? + end + + def restore! + if valid_format? + @padding = '0' * (10 - @identifier.length) + return(@full_number = @identifier.rjust(10, '0')) + end + + raise InvalidFormatError, "CIK '#{full_number}' is invalid and cannot be restored!" + end + end +end diff --git a/spec/sec_id/cik_spec.rb b/spec/sec_id/cik_spec.rb new file mode 100644 index 0000000..2fb57b5 --- /dev/null +++ b/spec/sec_id/cik_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +RSpec.describe SecId::CIK do + let(:cik) { described_class.new(cik_number) } + + context 'when CIK is valid' do + let(:cik_number) { '0001521365' } + + it 'parses CIK correctly' do + expect(cik.padding).to eq('000') + expect(cik.identifier).to eq('1521365') + expect(cik.check_digit).to be_nil + end + + describe '#valid?' do + it 'returns true' do + expect(cik.valid?).to be(true) + end + end + + describe '#restore!' do + it 'returns full CIK number' do + expect(cik.restore!).to eq(cik_number) + expect(cik.full_number).to eq(cik_number) + end + end + + describe '#calculate_check_digit' do + it 'raises an error' do + expect { cik.calculate_check_digit }.to raise_error(NotImplementedError) + end + end + end + + context 'when CIK number is missing leading zeros' do + let(:cik_number) { '10624' } + + it 'parses CIK number correctly' do + expect(cik.identifier).to eq(cik_number) + expect(cik.check_digit).to be_nil + end + + describe '#valid?' do + it 'returns true' do + expect(cik.valid?).to be(true) + end + end + + describe '#restore!' do + it 'returns full CIK number and sets padding' do + expect(cik.restore!).to eq('0000010624') + expect(cik.full_number).to eq('0000010624') + expect(cik.padding).to eq('00000') + end + end + + describe '#calculate_check_digit' do + it 'raises an error' do + expect { cik.calculate_check_digit }.to raise_error(NotImplementedError) + end + end + end + + describe '.valid?' do + context 'when CIK is malformed' do + it 'returns false' do + expect(described_class.valid?('X9')).to be(false) + expect(described_class.valid?('0000000000')).to be(false) + expect(described_class.valid?('01234567890')).to be(false) + end + end + + context 'when CIK is valid' do + it 'returns true' do + %w[0000000003 0000089562 0000010624 0002035979].each do |cik_number| + expect(described_class.valid?(cik_number)).to be(true) + end + end + end + end + + describe '.restore!' do + context 'when CIK is malformed' do + it 'raises an error' do + expect { described_class.restore!('X9') }.to raise_error(SecId::InvalidFormatError) + expect { described_class.restore!('0000000000') }.to raise_error(SecId::InvalidFormatError) + expect { described_class.restore!('09876543210') }.to raise_error(SecId::InvalidFormatError) + end + end + + context 'when CIK is valid' do + it 'restores check-digit and returns full CIK number' do + expect(described_class.restore!('3')).to eq('0000000003') + expect(described_class.restore!('0000000003')).to eq('0000000003') + expect(described_class.restore!('1072424')).to eq('0001072424') + expect(described_class.restore!('001072424')).to eq('0001072424') + expect(described_class.restore!('0001072424')).to eq('0001072424') + end + end + end + + describe '.valid_format?' do + context 'when CIK is malformed' do + it 'returns false' do + expect(described_class.valid_format?('X9')).to be(false) + expect(described_class.valid_format?('0000000000')).to be(false) + expect(described_class.valid_format?('01234567890')).to be(false) + end + end + + context 'when CIK is valid or missing leading zeros' do + it 'returns true' do + expect(described_class.valid_format?('3')).to be(true) + expect(described_class.valid_format?('0000000003')).to be(true) + expect(described_class.valid_format?('1072424')).to be(true) + expect(described_class.valid_format?('001072424')).to be(true) + expect(described_class.valid_format?('0001072424')).to be(true) + end + end + end + + describe '.check_digit' do + it 'raises an error' do + expect { described_class.check_digit('0000320193') }.to raise_error(NotImplementedError) + expect { described_class.check_digit('320193') }.to raise_error(NotImplementedError) + expect { described_class.check_digit('0') }.to raise_error(NotImplementedError) + end + end +end