diff --git a/.rubocop.yml b/.rubocop.yml index 44a2b1d..9fbc143 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,10 @@ -Documentation: +Style/Documentation: + Enabled: false + +Style/StringLiteralsInInterpolation: + Enabled: false + +Style/StringLiterals: Enabled: false Style/FileName: diff --git a/lib/coupon_code.rb b/lib/coupon_code.rb index 537fd1b..ce4aaba 100644 --- a/lib/coupon_code.rb +++ b/lib/coupon_code.rb @@ -1,46 +1,13 @@ -require 'coupon_code/version' require 'securerandom' require 'digest/sha1' +require 'coupon_code/version' +require 'coupon_code/generator' +require 'coupon_code/configuration' +require 'coupon_code/validator' module CouponCode SYMBOL = '0123456789ABCDEFGHJKLMNPQRTUVWXY' PARTS = 3 LENGTH = 4 - - def self.generate(options = { parts: PARTS }) - num_parts = options.delete(:parts) - parts = [] - (1..num_parts).each do |i| - part = '' - (1...LENGTH).each { part << random_symbol } - part << checkdigit_alg_1(part, i) - parts << part - end - parts.join('-') - end - - def self.validate(orig, num_parts = PARTS) - code = orig.upcase - code.gsub!(/[^0-9A-Z]+/, '') - parts = code.scan(/[0-9A-Z]{#{LENGTH}}/) - return if parts.length != num_parts - parts.each_with_index do |part, i| - data = part[0...(LENGTH - 1)] - check = part[-1] - return if check != checkdigit_alg_1(data, i + 1) - end - parts.join('-') - end - - def self.checkdigit_alg_1(orig, check) - orig.split('').each_with_index do |c, _| - k = SYMBOL.index(c) - check = check * 19 + k - end - SYMBOL[check % 31] - end - - def self.random_symbol - SYMBOL[rand(SYMBOL.length)] - end + SEPARATOR = '-' end diff --git a/lib/coupon_code/configuration.rb b/lib/coupon_code/configuration.rb new file mode 100644 index 0000000..b447bd6 --- /dev/null +++ b/lib/coupon_code/configuration.rb @@ -0,0 +1,28 @@ +module CouponCode + class << self + attr_accessor :configuration + end + + def self.config + @config ||= Configuration.new + end + + def self.configure + yield(config) + end + + def self.reset + @config = Configuration.new + end + + class Configuration + + attr_accessor :separator, :part_length, :num_parts + + def initialize + @separator = '-' + @part_length = 4 + @num_parts = 3 + end + end +end diff --git a/lib/coupon_code/generator.rb b/lib/coupon_code/generator.rb new file mode 100644 index 0000000..8fac117 --- /dev/null +++ b/lib/coupon_code/generator.rb @@ -0,0 +1,18 @@ +module CouponCode + def self.generate(options = { parts: PARTS }) + num_parts = options.delete(:parts) + parts = [] + (1..num_parts).each do |i| + part = '' + (1...LENGTH).each { part << random_symbol } + part << checkdigit_alg_1(part, i) + parts << part + end + parts.join('-') + end + + def self.random_symbol + SYMBOL[rand(SYMBOL.length)] + end + private_class_method :random_symbol +end diff --git a/lib/coupon_code/validator.rb b/lib/coupon_code/validator.rb new file mode 100644 index 0000000..61f2433 --- /dev/null +++ b/lib/coupon_code/validator.rb @@ -0,0 +1,23 @@ +module CouponCode + def self.validate(orig, num_parts = PARTS) + code = orig.upcase + code.gsub!(/[^0-9A-Z]+/, '') + parts = code.scan(/[0-9A-Z]{#{LENGTH}}/) + return if parts.length != num_parts + parts.each_with_index do |part, i| + data = part[0...(LENGTH - 1)] + check = part[-1] + return if check != checkdigit_alg_1(data, i + 1) + end + parts.join('-') + end + + def self.checkdigit_alg_1(orig, check) + orig.split('').each_with_index do |c, _| + k = SYMBOL.index(c) + check = check * 19 + k + end + SYMBOL[check % 31] + end + private_class_method :checkdigit_alg_1 +end diff --git a/test/configuration_test.rb b/test/configuration_test.rb new file mode 100644 index 0000000..af251a7 --- /dev/null +++ b/test/configuration_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +describe 'Configuration' do + it 'separator' do + config = CouponCode::Configuration.new + config.separator = '*' + config.separator.must_equal('*') + end +end diff --git a/test/generator_test.rb b/test/generator_test.rb new file mode 100644 index 0000000..e587946 --- /dev/null +++ b/test/generator_test.rb @@ -0,0 +1,34 @@ +require 'test_helper' + +describe CouponCode do + def generate(*args) + CouponCode.generate(*args) + end + + it 'should be loaded.' do + CouponCode.must_respond_to(:generate) + end + + it 'should generate a code' do + generate.wont_be_empty + end + + it 'should only contain uppercase letters, digits, and dashes.' do + generate.must_match(/^[0-9A-Z-]+$/) + end + + it 'should look like XXXX-XXXX-XXXX.' do + generate.must_match(/^\w{4}-\w{4}-\w{4}$/) + end + + it 'should generate different codes.' do + code1 = generate + code2 = generate + code1.wont_equal(code2) + end + + it 'should generate an arbitrary number of parts.' do + generate(parts: 2).must_match(/^\w{4}-\w{4}$/) + generate(parts: 5).must_match(/^\w{4}-\w{4}-\w{4}-\w{4}-\w{4}$/) + end +end diff --git a/test/validator_test.rb b/test/validator_test.rb new file mode 100644 index 0000000..db6108d --- /dev/null +++ b/test/validator_test.rb @@ -0,0 +1,63 @@ +require 'test_helper' + +describe Validator do + it 'should be loaded.' do + CouponCode.must_respond_to(:validate) + end + + it 'should fail to validate invalid code.' do + CouponCode.validate('').must_equal(nil) + end + + it 'should accept a valid code.' do + CouponCode.validate('1K7Q-CTFM-LMTC').wont_be_nil + end + + it 'should reject a short code.' do + CouponCode.validate('1K7Q-CTFM').must_be_nil + end + + it 'should accept a short code with correct parts.' do + CouponCode.validate('1K7Q-CTFM', 2).wont_be_nil + end + + it 'should reject a short code with wrong parts.' do + CouponCode.validate('CTFM-1K7Q', 2).must_be_nil + end + + it 'should fix and validate a lowercase code.' do + code = '1k7q-ctfm-lmtc' + CouponCode.validate(code.downcase).must_equal(code.upcase) + end + + it 'should validate alternative separators.' do + code = '1k7q/ctfm/lmtc' + CouponCode.validate(code).must_equal('1K7Q-CTFM-LMTC') + + code = '1k7q ctfm lmtc' + CouponCode.validate(code).must_equal('1K7Q-CTFM-LMTC') + + code = '1k7qctfmlmtc' + CouponCode.validate(code).must_equal('1K7Q-CTFM-LMTC') + end + + it 'should valid code-pretest.' do + CouponCode.validate('1K7Q', 1).wont_be_nil + CouponCode.validate('1K7C', 1).must_be_nil + + CouponCode.validate('1K7Q-CTFM', 2).wont_be_nil + CouponCode.validate('1K7Q-CTFW', 2).must_be_nil + + CouponCode.validate('1K7Q-CTFM-LMTC', 3).wont_be_nil + CouponCode.validate('1K7Q-CTFM-LMT1', 3).must_be_nil + + CouponCode.validate('7YQH-1FU7-E1HX-0BG9', 4).wont_be_nil + CouponCode.validate('7YQH-1FU7-E1HX-0BGP', 4).must_be_nil + + CouponCode.validate('YENH-UPJK-PTE0-20U6-QYME', 5).wont_be_nil + CouponCode.validate('YENH-UPJK-PTE0-20U6-QYMT', 5).must_be_nil + + CouponCode.validate('YENH-UPJK-PTE0-20U6-QYME-RBK1', 6).wont_be_nil + CouponCode.validate('YENH-UPJK-PTE0-20U6-QYME-RBK2', 6).must_be_nil + end +end