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.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.simplecov +4 -0
- data/.travis.yml +10 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +75 -0
- data/Rakefile +1 -0
- data/has_unique_three_letter_code.gemspec +28 -0
- data/lib/has_unique_three_letter_code.rb +7 -0
- data/lib/has_unique_three_letter_code/class_methods.rb +32 -0
- data/lib/has_unique_three_letter_code/configuration.rb +48 -0
- data/lib/has_unique_three_letter_code/instance_methods.rb +91 -0
- data/lib/has_unique_three_letter_code/version.rb +3 -0
- data/spec/configuration_spec.rb +32 -0
- data/spec/has_unique_three_letter_column_spec.rb +212 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/migration.rb +14 -0
- data/spec/support/model.rb +9 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -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=
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.simplecov
ADDED
data/.travis.yml
ADDED
@@ -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
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|