has_unique_three_letter_code 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NjQzOTJkMmY4ZTdhOThmNjMxN2MwZmIyYjMxMDU1ZDExM2JhMmY4Mw==
5
+ data.tar.gz: !binary |-
6
+ YTMwYzRmYzE1ZmFhZjNlYTFmZTdlNmQ1YmNlNTM5YmI3N2FhY2NhNg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OWViOWRhMzNkYmY5MzJlNGU0OWEwOTlmNDVmMWQ2NmI5YjQ5ZjNiMDZlMjE5
10
+ NzQ1MTcxZTFlYmRlYTgxMDNhN2RkODNiNWY0ZjY5NDYxY2YzMDFhNDkwNzBl
11
+ ZjA0NGRjMzgyMDZlYWRkOGMwNTQ0ZTI3NzdhNWM4MWJhNjUzNjE=
12
+ data.tar.gz: !binary |-
13
+ MWY4NDEyNGQwNmU3MTg3MDdmZTA1N2NmZTI3NGY4M2NiNDFhZGM5YzBlZjRj
14
+ Mzg4YzVjZGZiNTM0MDk2NDg5ODU4NjQxYzhjNTg0OTdhMjcwZGVkNWQxZmIz
15
+ YWMxOGY1NWQwYjE5ZTg0YjlhMThiZDI4YjQ3YTYwN2U5YzJjMjk=
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,4 @@
1
+ if ENV['COVERAGE']
2
+ SimpleCov.start 'test_frameworks' do
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ before_script:
2
+ - bundle install
3
+ - mysql -e 'create database myapp_test;'
4
+ - |
5
+ echo "test:
6
+ adapter: mysql2
7
+ database: myapp_test
8
+ username: travis
9
+ encoding: utf8" > ${HOME}/has_unique_three_letter_code.yml
10
+ script: BCDATABASE_PATH=${HOME} COVERAGE=true rspec
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in has_unique_three_letter_code.gemspec
4
+ gemspec
5
+ gem 'simplecov', :require => false
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Isaac Betesh
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ # HasUniqueThreeLetterCode
2
+
3
+ User requirements: Assign a case-insensitive unique three-letter code to each record in a scope, based loosely on some other attribute of the record.
4
+
5
+ Example: Within the office, we refer to each client by a TLA (three-letter acronym). We want to store the TLA's in our database automatically when we create a client, and we want the TLA to be loosely based on the client's name. The National Rifle Association is the NRA. The Office of Management and Budget is the OMB. The National Restaurant Association--well, NRA is already taken, so how about NRX?
6
+
7
+ Solution: HasUniqueThreeLetterCode
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'has_unique_three_letter_code'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install has_unique_three_letter_code
22
+
23
+ ## Usage
24
+
25
+ class Client < ActiveRecord::Base
26
+ attr_accessible :tla, :name, :tla_group
27
+
28
+ include HasUniqueThreeLetterCode
29
+ def needs_tla?
30
+ # Maybe clients that went out of business can have their TLA retired.
31
+ # Or maybe you don't need a TLA for a client with a monosyllabic name
32
+ end
33
+ has_unique_three_letter_code :tla,
34
+ :source => :name, # The field we use to generate the TLA. This can be another column in this table, or any other method an instance of the class responds_to?, so if it's in another table, just `delegate :name, :to => :the_associated_record`
35
+ :uniqueness => { :scope => :tla_group }, # uniqueness constraints are passed directly to validates_uniqueness_of
36
+ :leave_blank => { :unless => :needs_tla? } # :leave_blank supports keys :if and :unless. Values can be Procs or symbolized instance method names.
37
+ end
38
+
39
+
40
+ ## Testing
41
+
42
+ Create a file at /etc/nubic/db/has_unique_three_letter_code.yml with your database configuration nested under 'test':
43
+
44
+ Example:
45
+
46
+ test:
47
+ adapter: mysql2
48
+ encoding: utf8
49
+ host: ...
50
+ port: ...
51
+ database: ...
52
+ pool: 5
53
+ username: ...
54
+ password: ...
55
+
56
+ Then run:
57
+
58
+ $ COVERAGE=true rspec
59
+ (The environment variable 'COVERAGE' is optional and runs it with simplecov)
60
+
61
+ ## Future development possibilities
62
+
63
+ 1. Support options for lowercase and case-sensitive.
64
+ 2. Support N-letter codes.
65
+ 3. Before resorting to a random letter, search for an available combination using the first two letters of a word.
66
+ 4. When choosing a random letter, run through them randomly without repetition, instead of alphabetically.
67
+ 5. Add support for Rails 4
68
+
69
+ ## Contributing
70
+
71
+ 1. Fork it
72
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
73
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
74
+ 4. Push to the branch (`git push origin my-new-feature`)
75
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'has_unique_three_letter_code/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "has_unique_three_letter_code"
8
+ spec.version = HasUniqueThreeLetterCode::VERSION
9
+ spec.authors = ["Isaac Betesh"]
10
+ spec.email = ["iybetesh@gmail.com"]
11
+ spec.description = "Assigns a case-insensitive unique three-letter code to each record in a scope, based loosely on some other attribute of the record"
12
+ spec.summary = `cat README.md`
13
+ spec.homepage = "https://github.com/betesh/has_unique_three_letter_code/"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "bcdatabase"
25
+ spec.add_development_dependency "mysql2"
26
+
27
+ spec.add_runtime_dependency "activerecord", "~> 3.2.0"
28
+ end
@@ -0,0 +1,7 @@
1
+ require "has_unique_three_letter_code/configuration"
2
+ require "has_unique_three_letter_code/class_methods"
3
+ require "has_unique_three_letter_code/instance_methods"
4
+ require "has_unique_three_letter_code/version"
5
+
6
+ module HasUniqueThreeLetterCode
7
+ end
@@ -0,0 +1,32 @@
1
+ require "active_record/validations"
2
+
3
+ module HasUniqueThreeLetterCode
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ attr_reader :code_sources
8
+ def has_unique_three_letter_code(*columns)
9
+ options = columns.last.is_a?(Hash) ? columns.pop : {}
10
+ columns.each do |column|
11
+ validates column,
12
+ :uniqueness => (options[:uniqueness] || true),
13
+ :length => {:is => 3, :allow_nil => true}.merge(options[:length] || {}),
14
+ :exclusion => { :in => Proc.new { HasUniqueThreeLetterCode.config.forbidden_codes }, :message => "'%{value}' is not an appropriate word" }
15
+ (@code_sources ||= {})[column] = options[:source] || :name
16
+ before_validation do
17
+ if options[:leave_blank]
18
+ leave_blank = options[:leave_blank][:if]
19
+ if (leave_blank.blank? || (leave_blank.is_a?(Proc) && !leave_blank.call(self)) || (leave_blank.is_a?(Symbol) && !self.__send__(leave_blank)))
20
+ dont_leave_blank = options[:leave_blank][:unless]
21
+ if (dont_leave_blank.blank? || (dont_leave_blank.is_a?(Proc) && dont_leave_blank.call(self)) || (dont_leave_blank.is_a?(Symbol) && self.__send__(dont_leave_blank)))
22
+ set_three_letter_column column if self.__send__(column).blank?
23
+ end
24
+ end
25
+ else
26
+ set_three_letter_column column if self.__send__(column).blank?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,48 @@
1
+ module HasUniqueThreeLetterCode
2
+ DEFAULT_FORBIDDEN_CODES = ['AID', 'AIL', 'ALL', 'ANA', 'AND', 'APP', 'ASA', 'ASK', 'ASS', 'BAD', 'BED', 'BOA', 'BOB', 'BOY', 'BRA', 'BRO', 'BUG', 'BUM', 'BUN', 'BUY', 'CRA', 'CRP', 'CUM', 'CUN', 'DID', 'DIE', 'DIK', 'DOA', 'DOG', 'EEL', 'FAG', 'FAT', 'FEE', 'FLU', 'FOO', 'FUC', 'FUK', 'FUN', 'GAG', 'GAY', 'GET', 'GOD', 'GOO', 'GOT', 'GOY', 'GUN', 'GUY', 'HAD', 'HAM', 'HEY', 'HIT', 'HOE', 'HOG', 'HOT', 'ICK', 'ILL', 'JEW', 'JIZ', 'LAW', 'LOO', 'MAX', 'MEX', 'MIN', 'MOB', 'MOM', 'MOO', 'NAG', 'NEG', 'NIG', 'NIP', 'ODD', 'OXY', 'PAY', 'PEE', 'PIE', 'PIG', 'POT', 'PUB', 'PUG', 'PUS', 'PUS', 'RAG', 'RED', 'REP', 'RET', 'RIM', 'SAD', 'SAG', 'SEX', 'SHI', 'SHT', 'SOB', 'SOL', 'SUB', 'SUE', 'SUK', 'TAX', 'TOI', 'TOY', 'URN', 'USA', 'WAD', 'WAP', 'WAY', 'WET', 'WIN', 'WOE', 'WOP', 'WOW', 'YEH', 'ZIT', 'ZOO']
3
+
4
+ class << self
5
+ def config
6
+ @config ||= Configuration.new
7
+ end
8
+ def configure
9
+ yield @config
10
+ end
11
+ end
12
+
13
+ class Configuration
14
+ THREE_LETTERS = /[A-Z]{3}/
15
+
16
+ attr_reader :forbidden_codes
17
+
18
+ def initialize
19
+ reset
20
+ end
21
+
22
+ def reset
23
+ self.forbidden_codes = DEFAULT_FORBIDDEN_CODES.dup
24
+ end
25
+
26
+ def forbidden_codes=(_)
27
+ raise ::ArgumentError, "config.forbidden_codes must be an array of 3-character strings. For example: `config.forbidden_codes = ['ABC', 'DEF', 'GHI']`" unless _.is_a?(Array)
28
+
29
+ _.reject { |code| 3 == code.size }.tap do |wrong_size|
30
+ if 1 == wrong_size.size
31
+ raise ::ArgumentError, "config.forbidden_codes contains an element that is the wrong length: #{wrong_size.first}"
32
+ elsif 1 < wrong_size.size
33
+ raise ::ArgumentError, "config.forbidden_codes contains elements that are the wrong length: #{wrong_size.join(', ')}"
34
+ end
35
+ end
36
+
37
+ _.reject { |code| THREE_LETTERS.match(code) }.tap do |non_alpha|
38
+ if 1 == non_alpha.size
39
+ raise ::ArgumentError, "config.forbidden_codes contains an element with characters other than capital letters: #{non_alpha.first}"
40
+ elsif 1 < non_alpha.size
41
+ raise ::ArgumentError, "config.forbidden_codes contains elements with characters other than capital letters: #{non_alpha.join(', ')}"
42
+ end
43
+ end
44
+
45
+ @forbidden_codes = _
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,91 @@
1
+ module HasUniqueThreeLetterCode
2
+ ALPHABET_ARRAY = ('A'..'Z').to_a
3
+ private
4
+ def set_three_letter_column(_)
5
+ return unless self.__send__(_).blank?
6
+ deterministic_three_letter_column(_)
7
+ random_three_letter_column(_) until unique_three_letter_column(_)
8
+ end
9
+
10
+ def deterministic_three_letter_column(_)
11
+ value = source_value(_)
12
+ return random_three_letter_column(_) if value.blank?
13
+ acronym = value.gsub(/\s+/m, ' ').gsub(/^\s+|\s+$/m, '').split(' ').collect(&:first).join.gsub(/[^A-Za-z]/, '').upcase
14
+ if acronym.size >= 3 then
15
+ acronym.size.times do |i|
16
+ (i+1..acronym.size-1).each do |j|
17
+ (j+1..acronym.size-1).each do |k|
18
+ self.__send__("#{_}=", "#{acronym[i]}#{acronym[j]}#{acronym[k]}")
19
+ return if unique_three_letter_column(_)
20
+ end
21
+ end
22
+ (i+1..acronym.size-1).each do |j|
23
+ ALPHABET_ARRAY.each do |k|
24
+ self.__send__("#{_}=", "#{acronym[i]}#{acronym[j]}#{k}")
25
+ return if unique_three_letter_column(_)
26
+ end
27
+ end
28
+ (i+1..acronym.size-1).each do |j|
29
+ three_letter_column_with_two_fixed_adjacent_letters(_, "#{acronym[i]}#{acronym[j]}")
30
+ return if unique_three_letter_column(_)
31
+ end
32
+ (i+1..acronym.size-1).each do |j|
33
+ three_letter_column_with_two_fixed_split_letters(_, acronym[i], acronym[j])
34
+ return if unique_three_letter_column(_)
35
+ end
36
+ three_letter_column_with_one_fixed_letter(_, acronym[i])
37
+ return if unique_three_letter_column(_)
38
+ end
39
+ elsif 2 == acronym.size
40
+ three_letter_column_with_two_fixed_adjacent_letters(_, acronym)
41
+ return if unique_three_letter_column(_)
42
+ three_letter_column_with_one_fixed_letter(_, acronym[0])
43
+ return if unique_three_letter_column(_)
44
+ three_letter_column_with_one_fixed_letter(_, acronym[1])
45
+ elsif 1 == acronym.size
46
+ three_letter_column_with_one_fixed_letter(_, acronym)
47
+ end
48
+ end
49
+
50
+ def random_char
51
+ ALPHABET_ARRAY[Random.rand(0..25)]
52
+ end
53
+
54
+ def unique_three_letter_column(_)
55
+ begin
56
+ valid? || self.errors[_].empty?
57
+ rescue => e
58
+ puts e.backtrace.join("\n")
59
+ raise e
60
+ end
61
+ end
62
+
63
+ def random_three_letter_column(_)
64
+ self.__send__("#{_}=", "#{random_char}#{random_char}#{random_char}")
65
+ end
66
+
67
+ def three_letter_column_with_one_fixed_letter(col, _)
68
+ ALPHABET_ARRAY.each do |i|
69
+ three_letter_column_with_two_fixed_adjacent_letters(col, "#{_}#{i}")
70
+ return if unique_three_letter_column(col)
71
+ end
72
+ end
73
+
74
+ def three_letter_column_with_two_fixed_adjacent_letters(col, _)
75
+ ALPHABET_ARRAY.each do |i|
76
+ self.__send__("#{col}=", "#{_}#{i}")
77
+ return if unique_three_letter_column(col)
78
+ end
79
+ end
80
+
81
+ def three_letter_column_with_two_fixed_split_letters(col, i, k)
82
+ ALPHABET_ARRAY.each do |j|
83
+ self.__send__("#{col}=", "#{i}#{j}#{k}")
84
+ return if unique_three_letter_column(col)
85
+ end
86
+ end
87
+
88
+ def source_value(_)
89
+ __send__(self.class.code_sources[_])
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module HasUniqueThreeLetterCode
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe HasUniqueThreeLetterCode do
4
+ describe "forbidden_codes" do
5
+ def set(_)
6
+ described_class.config.forbidden_codes=_
7
+ end
8
+ it "cannot be nil" do
9
+ expect{set(nil)}.to raise_error(ArgumentError, "config.forbidden_codes must be an array of 3-character strings. For example: `config.forbidden_codes = ['ABC', 'DEF', 'GHI']`")
10
+ end
11
+ it "cannot have a 4-character string" do
12
+ expect{set(['ABCD'])}.to raise_error(ArgumentError, "config.forbidden_codes contains an element that is the wrong length: ABCD")
13
+ end
14
+ it "cannot have a 2-character string" do
15
+ expect{set(['AB'])}.to raise_error(ArgumentError, "config.forbidden_codes contains an element that is the wrong length: AB")
16
+ end
17
+ it "cannot have 2 elements with the wrong number of characters" do
18
+ expect{set(['ABCD', 'EF'])}.to raise_error(ArgumentError, "config.forbidden_codes contains elements that are the wrong length: ABCD, EF")
19
+ end
20
+ it "cannot have an element with a non-alpha character" do
21
+ expect{set(['AB1'])}.to raise_error(ArgumentError, "config.forbidden_codes contains an element with characters other than capital letters: AB1")
22
+ end
23
+ it "cannot have an element with a non-alpha character" do
24
+ expect{set(['AB1', '2CD'])}.to raise_error(ArgumentError, "config.forbidden_codes contains elements with characters other than capital letters: AB1, 2CD")
25
+ end
26
+ it "can be an empty array" do
27
+ expect{set([])}.to_not raise_error
28
+ end
29
+ end
30
+
31
+ after(:all) { described_class.config.reset }
32
+ end
@@ -0,0 +1,212 @@
1
+ require_relative 'spec_helper'
2
+
3
+ def tla_of(_)
4
+ _.tap(&:valid?).tla
5
+ end
6
+
7
+ RSpec::Matchers.define :be_assigned_tla do |expected|
8
+ match do |actual|
9
+ tla_of(actual) == expected
10
+ end
11
+ end
12
+
13
+ RSpec::Matchers.define :be_assigned_any_3_letter_tla do
14
+ match do |actual|
15
+ /\A[A-Z]{3}\z/.match(tla_of(actual))
16
+ end
17
+ end
18
+
19
+ RSpec::Matchers.define :be_3_letters_beginning_with do |expected|
20
+ match do |actual|
21
+ /\A#{expected}[A-Z]{#{3 - expected.size}}\z/.match(tla_of(actual))
22
+ end
23
+ end
24
+
25
+
26
+ describe Client do
27
+ describe "tla" do
28
+ def given_tla_is_unavailable(_)
29
+ HasUniqueThreeLetterCode.config.forbidden_codes << _
30
+ end
31
+
32
+ def given_all_codes_are_taken_that_begin_with(_)
33
+ if 1 == _.size
34
+ Client::ALPHABET_ARRAY.each do |a|
35
+ Client::ALPHABET_ARRAY.each do |b|
36
+ given_tla_is_unavailable("#{_}#{a}#{b}")
37
+ end
38
+ end
39
+ elsif 2 == _.size
40
+ Client::ALPHABET_ARRAY.each do |a|
41
+ given_tla_is_unavailable("#{_}#{a}")
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '> 3 letters in initials' do
47
+ let(:target) { Client.new(:name => "John Deere Tractors & Farm Equipment, Ltd.") }
48
+
49
+ it "should be all caps" do
50
+ Client.new(:name => 'lower case letters').should be_assigned_tla('LCL')
51
+ end
52
+
53
+ it "should be the 1st 3 initials of business name" do
54
+ target.should be_assigned_tla('JDT')
55
+ end
56
+
57
+ it "should be the 1st, 2nd and 4th initials in business name when 1st 3 are non-unique" do
58
+ given_tla_is_unavailable('JDT')
59
+ target.should be_assigned_tla('JDF')
60
+ end
61
+
62
+ it "should be the 1st, 3rd and 4th initials in business name when 1st 2 and any later ones are non-unique" do
63
+ ['JDT', 'JDF', 'JDE', 'JDL'].each do |_|
64
+ given_tla_is_unavailable(_)
65
+ end
66
+ target.should be_assigned_tla('JTF')
67
+ end
68
+
69
+ it "should be the 1st, 3rd and 5th initials in business name when 1st 2 and any later ones are non-unique and 1st, 3rd and 4th are non-unique" do
70
+ ['JDT', 'JDF', 'JDE', 'JDL', 'JTF'].each do |_|
71
+ given_tla_is_unavailable(_)
72
+ end
73
+ target.should be_assigned_tla('JTE')
74
+ end
75
+
76
+ it "should begin with the 1st 2 initials in business name when 1st 2 with any later one is non-unique" do
77
+ ['JDT', 'JDF', 'JDE', 'JDL', 'JTF', 'JTE', 'JTL', 'JFE', 'JFL', 'JEL'].each do |_|
78
+ given_tla_is_unavailable(_)
79
+ end
80
+ target.should be_assigned_tla('JDA')
81
+ end
82
+
83
+ it "should begin with the 1st and 3rd initial in business name when 1st 2 with any letter and 1st and 3rd with any later initial is non-unique" do
84
+ Client::ALPHABET_ARRAY.each do |_|
85
+ given_tla_is_unavailable("JD#{_}")
86
+ end
87
+ ['JTF', 'JTE', 'JTL', 'JFE', 'JFL', 'JEL'].each do |_|
88
+ given_tla_is_unavailable(_)
89
+ end
90
+ target.should be_assigned_tla('JTA')
91
+ end
92
+
93
+ it "should be the 1st and last initial in business name with another letter after them when no other ordered combination of 2 initials from the business_name is unique with any 3rd letter" do
94
+ Client::ALPHABET_ARRAY.each do |_|
95
+ given_tla_is_unavailable("JD#{_}")
96
+ given_tla_is_unavailable("JT#{_}")
97
+ given_tla_is_unavailable("JF#{_}")
98
+ given_tla_is_unavailable("JE#{_}")
99
+ end
100
+ target.should be_assigned_tla('JLA')
101
+ end
102
+
103
+ it "should be the 1st and 2nd initial in business name with another letter between them when no ordered combination of 2 initials from the business_name is unique with any 3rd letter" do
104
+ Client::ALPHABET_ARRAY.each do |_|
105
+ given_tla_is_unavailable("JD#{_}")
106
+ given_tla_is_unavailable("JT#{_}")
107
+ given_tla_is_unavailable("JF#{_}")
108
+ given_tla_is_unavailable("JE#{_}")
109
+ given_tla_is_unavailable("JL#{_}")
110
+ end
111
+ target.should be_assigned_tla('JAD')
112
+ end
113
+
114
+ it "should begin with the 1st initial in business name when any ordered combination of initials is non-unique" do
115
+ given_all_codes_are_taken_that_begin_with('JD')
116
+ ['JTF', 'JTE', 'JTL', 'JFE', 'JFL', 'JEL'].each do |_|
117
+ given_tla_is_unavailable(_)
118
+ end
119
+ target.should be_assigned_tla('JTA')
120
+ end
121
+
122
+ it "should be the 2nd, 3rd and 4th initial in business name when any 3-letter combination beginning with the first letter is non-unique" do
123
+ given_all_codes_are_taken_that_begin_with('J')
124
+ target.should be_assigned_tla('DTF')
125
+ end
126
+
127
+ it "should be the 2nd, 3rd and 5th initial in business name when any 3-letter combination beginning with the first letter, and the 2nd, 3rd and 4th, are non-unique" do
128
+ given_all_codes_are_taken_that_begin_with('J')
129
+ given_tla_is_unavailable('DTF')
130
+ target.should be_assigned_tla('DTE')
131
+ end
132
+
133
+ it "should begin with the last initial when all codes beginning with any other initials in business name are taken" do
134
+ 'JDTFE'.each_char do |_|
135
+ given_all_codes_are_taken_that_begin_with(_)
136
+ end
137
+ target.should be_assigned_tla('LAA')
138
+ end
139
+
140
+ it "should be random when all codes beginning with any initials in business name are taken" do
141
+ 'JDTFEL'.each_char do |_|
142
+ given_all_codes_are_taken_that_begin_with(_)
143
+ end
144
+ target.should be_assigned_any_3_letter_tla
145
+ end
146
+ end
147
+
148
+ describe '2 letters in initials' do
149
+ let(:target) { Client.new(:name => 'Teddy Bears') }
150
+
151
+ it "should be the 1st 2 initials of business name plus a random letter" do
152
+ target.should be_3_letters_beginning_with('TB')
153
+ end
154
+
155
+ it "should be the 1st initial of business name plus two random letters if all ordered combinations of the 1st 2 initials plus a 3rd letter are taken" do
156
+ given_all_codes_are_taken_that_begin_with('TB')
157
+ target.should be_3_letters_beginning_with('T')
158
+ end
159
+
160
+ it "should be random if all ordered combinations beginning with the first letter are taken" do
161
+ given_all_codes_are_taken_that_begin_with('T')
162
+ target.should be_assigned_any_3_letter_tla
163
+ end
164
+ end
165
+
166
+
167
+ describe '1 letter initials' do
168
+ let (:target) { Client.new(:name => 'Switzerland') }
169
+
170
+ it "should be the initial of business name plus 2 random letters" do
171
+ target.should be_3_letters_beginning_with('S')
172
+ end
173
+
174
+ it "should be random if all ordered combinations beginning with the first letter are taken" do
175
+ given_all_codes_are_taken_that_begin_with('S')
176
+ target.should be_assigned_any_3_letter_tla
177
+ end
178
+ end
179
+
180
+ it "should be random if business_name is blank" do
181
+ Client.new.should be_assigned_any_3_letter_tla
182
+ end
183
+ end
184
+
185
+ describe "scoping uniqueness" do
186
+ it "should not allow two clients in the same tla_group to share a TLA" do
187
+ Client.create!(:name => 'A B C D', :tla_group => 1).should be_assigned_tla('ABC')
188
+ Client.create!(:name => 'A B C D', :tla_group => 1).should be_assigned_tla('ABD')
189
+ end
190
+
191
+ it "should allow two clients in different tla_groups to share a TLA" do
192
+ Client.create!(:name => 'A B C D', :tla_group => 1).should be_assigned_tla('ABC')
193
+ Client.create!(:name => 'A B C D', :tla_group => 2).should be_assigned_tla('ABC')
194
+ end
195
+ end
196
+
197
+ describe "leaving blank" do
198
+ it "should leave the TLA blank if configured with leave_blank" do
199
+ Client.create!(:name => 'A B C D', :needs_tla => false).should be_assigned_tla(nil)
200
+ end
201
+ end
202
+
203
+ describe "forbidden_codes" do
204
+ it "should not allow a TLA that is a forbidden code" do
205
+ Client.new(:name => 'A B C D').should be_assigned_tla('ABC')
206
+ HasUniqueThreeLetterCode.config.forbidden_codes = ['ABC']
207
+ Client.new(:name => 'A B C D').should be_assigned_tla('ABD')
208
+ end
209
+ end
210
+
211
+ after(:each) { HasUniqueThreeLetterCode.config.reset }
212
+ end
@@ -0,0 +1,25 @@
1
+ require "simplecov"
2
+
3
+ require "active_record"
4
+ require "bcdatabase"
5
+ ActiveRecord::Base.establish_connection(Bcdatabase.load[:has_unique_three_letter_code, "test"])
6
+
7
+ require "has_unique_three_letter_code"
8
+
9
+ Dir[File.expand_path("#{__FILE__}/../support/*.rb")].each { |f| require f }
10
+
11
+ RSpec.configure do |config|
12
+ config.order = "random"
13
+ config.before(:suite) do
14
+ CreateClients.up
15
+ end
16
+ config.after(:suite) do
17
+ CreateClients.down
18
+ end
19
+ config.around(:each) do |test|
20
+ Client.transaction do
21
+ test.run
22
+ raise ActiveRecord::Rollback
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ class CreateClients < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :clients do |t|
4
+ t.string :tla
5
+ t.string :name
6
+ t.integer :tla_group, :default => 0
7
+ t.boolean :needs_tla, :default => true
8
+ t.timestamps
9
+ end
10
+ end
11
+ def self.down
12
+ drop_table :clients
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ class Client < ActiveRecord::Base
2
+ attr_accessible :tla, :name, :needs_tla, :tla_group
3
+
4
+ include HasUniqueThreeLetterCode
5
+
6
+ has_unique_three_letter_code :tla, :source => :name,
7
+ :uniqueness => { :scope => :tla_group },
8
+ :leave_blank => { :unless => :needs_tla }
9
+ end
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_unique_three_letter_code
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Isaac Betesh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bcdatabase
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mysql2
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 3.2.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: 3.2.0
97
+ description: Assigns a case-insensitive unique three-letter code to each record in
98
+ a scope, based loosely on some other attribute of the record
99
+ email:
100
+ - iybetesh@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - .gitignore
106
+ - .rspec
107
+ - .simplecov
108
+ - .travis.yml
109
+ - Gemfile
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - has_unique_three_letter_code.gemspec
114
+ - lib/has_unique_three_letter_code.rb
115
+ - lib/has_unique_three_letter_code/class_methods.rb
116
+ - lib/has_unique_three_letter_code/configuration.rb
117
+ - lib/has_unique_three_letter_code/instance_methods.rb
118
+ - lib/has_unique_three_letter_code/version.rb
119
+ - spec/configuration_spec.rb
120
+ - spec/has_unique_three_letter_column_spec.rb
121
+ - spec/spec_helper.rb
122
+ - spec/support/migration.rb
123
+ - spec/support/model.rb
124
+ homepage: https://github.com/betesh/has_unique_three_letter_code/
125
+ licenses:
126
+ - MIT
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.1.5
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: ! '# HasUniqueThreeLetterCode User requirements: Assign a case-insensitive
148
+ unique three-letter code to each record in a scope, based loosely on some other
149
+ attribute of the record. Example: Within the office, we refer to each client by
150
+ a TLA (three-letter acronym). We want to store the TLA''s in our database automatically
151
+ when we create a client, and we want the TLA to be loosely based on the client''s
152
+ name. The National Rifle Association is the NRA. The Office of Management and
153
+ Budget is the OMB. The National Restaurant Association--well, NRA is already taken,
154
+ so how about NRX? Solution: HasUniqueThreeLetterCode ## Installation Add this
155
+ line to your application''s Gemfile: gem ''has_unique_three_letter_code'' And
156
+ then execute: $ bundle Or install it yourself as: $ gem install has_unique_three_letter_code ##
157
+ Usage class Client < ActiveRecord::Base attr_accessible :tla, :name, :tla_group include
158
+ HasUniqueThreeLetterCode def needs_tla? # Maybe clients that went out of business
159
+ can have their TLA retired. # Or maybe you don''t need a TLA for a client with a
160
+ monosyllabic name end has_unique_three_letter_code :tla, :source => :name, # The
161
+ field we use to generate the TLA. This can be another column in this table, or
162
+ any other method an instance of the class responds_to?, so if it''s in another table,
163
+ just `delegate :name, :to => :the_associated_record` :uniqueness => { :scope =>
164
+ :tla_group }, # uniqueness constraints are passed directly to validates_uniqueness_of
165
+ :leave_blank => { :unless => :needs_tla? } # :leave_blank supports keys :if and
166
+ :unless. Values can be Procs or symbolized instance method names. end ## Testing Create
167
+ a file at /etc/nubic/db/has_unique_three_letter_code.yml with your database configuration
168
+ nested under ''test'': Example: test: adapter: mysql2 encoding: utf8 host: ...
169
+ port: ... database: ... pool: 5 username: ... password: ... Then run: $ COVERAGE=true
170
+ rspec (The environment variable ''COVERAGE'' is optional and runs it with simplecov) ##
171
+ Future development possibilities 1. Support options for lowercase and case-sensitive.
172
+ 2. Support N-letter codes. 3. Before resorting to a random letter, search for an
173
+ available combination using the first two letters of a word. 4. When choosing a
174
+ random letter, run through them randomly without repetition, instead of alphabetically.
175
+ 5. Add support for Rails 4 ## Contributing 1. Fork it 2. Create your feature branch
176
+ (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am ''Add
177
+ some feature''`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create
178
+ new Pull Request'
179
+ test_files:
180
+ - spec/configuration_spec.rb
181
+ - spec/has_unique_three_letter_column_spec.rb
182
+ - spec/spec_helper.rb
183
+ - spec/support/migration.rb
184
+ - spec/support/model.rb