smartname 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +21 -19
- data/VERSION +1 -1
- data/lib/smart_name.rb +32 -223
- data/lib/smart_name/contextual.rb +113 -0
- data/lib/smart_name/manipulate.rb +73 -0
- data/lib/smart_name/parts.rb +124 -0
- data/lib/smart_name/predicates.rb +39 -0
- data/lib/smart_name/variants.rb +36 -0
- data/spec/lib/smart_name/contextual_spec.rb +133 -0
- data/spec/lib/smart_name/manipulate_spec.rb +75 -0
- data/spec/lib/smart_name/parts_spec.rb +65 -0
- data/spec/lib/smart_name/variants_spec.rb +44 -0
- data/spec/lib/smart_name_spec.rb +50 -183
- data/spec/spec_helper.rb +1 -0
- metadata +11 -2
@@ -0,0 +1,73 @@
|
|
1
|
+
class SmartName
|
2
|
+
module Manipulate
|
3
|
+
# replace a subname
|
4
|
+
# keys are used for comparison
|
5
|
+
def replace old, new
|
6
|
+
old_name = old.to_name
|
7
|
+
new_name = new.to_name
|
8
|
+
return self if old_name.length > length
|
9
|
+
return replace_part(old_name, new_name) if old_name.simple?
|
10
|
+
return self unless include? old_name
|
11
|
+
replace_all_subsequences(old_name, new_name).to_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def replace_part oldpart, newpart
|
15
|
+
ensure_simpliness oldpart, "Use 'replace' to replace junctions"
|
16
|
+
|
17
|
+
oldpart = oldpart.to_name
|
18
|
+
newpart = newpart.to_name
|
19
|
+
|
20
|
+
parts.map do |p|
|
21
|
+
oldpart == p ? newpart : p
|
22
|
+
end.to_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def replace_piece oldpiece, newpiece
|
26
|
+
oldpiece = oldpiece.to_name
|
27
|
+
newpiece = newpiece.to_name
|
28
|
+
|
29
|
+
return replace_part oldpiece, newpiece if oldpiece.simple?
|
30
|
+
return self unless self.starts_with?(oldpiece)
|
31
|
+
return newpiece if oldpiece.length == length
|
32
|
+
newpiece + self[oldpiece.length..-1]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def replace_all_subsequences oldseq, newseq
|
38
|
+
res = []
|
39
|
+
i = 0
|
40
|
+
while i <= length - oldseq.length
|
41
|
+
# for performance reasons: check first character first then the rest
|
42
|
+
if oldseq.part_keys.first == part_keys[i] &&
|
43
|
+
oldseq.part_keys == part_keys[i, oldseq.length]
|
44
|
+
res += newseq.parts
|
45
|
+
i += oldseq.length
|
46
|
+
else
|
47
|
+
res << parts[i]
|
48
|
+
i += 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
res += parts[i..-1] if i < length
|
52
|
+
res
|
53
|
+
end
|
54
|
+
|
55
|
+
def ensure_simpliness part, msg=nil
|
56
|
+
return if part.to_name.simple?
|
57
|
+
raise StandardError, "'#{part}' has to be simple. #{msg}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# ~~~~~~~~~~~~~~~~~~~~ MISC ~~~~~~~~~~~~~~~~~~~~
|
62
|
+
|
63
|
+
# HACK. This doesn't belong here.
|
64
|
+
# shouldn't it use inclusions???
|
65
|
+
def self.substitute! str, hash
|
66
|
+
hash.keys.each do |var|
|
67
|
+
str.gsub! var_re do |x|
|
68
|
+
hash[var.to_sym]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
str
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
class SmartName
|
2
|
+
# naming conventions:
|
3
|
+
# methods that end with _name return name objects
|
4
|
+
# the same methods without _name return strings
|
5
|
+
module Parts
|
6
|
+
attr_reader :parts, :part_keys, :simple
|
7
|
+
|
8
|
+
alias simple? simple
|
9
|
+
alias_method :to_a, :parts
|
10
|
+
alias_method :to_ary, :parts
|
11
|
+
|
12
|
+
def to_ary
|
13
|
+
if parts.empty?
|
14
|
+
[""]
|
15
|
+
else
|
16
|
+
parts
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize_parts
|
21
|
+
# -1 = don't suppress trailing null fields
|
22
|
+
@parts = @s.split(/\s*#{JOINT_RE}\s*/, -1)
|
23
|
+
@simple = @parts.size <= 1
|
24
|
+
# simple check needed to avoid inifinite recursion
|
25
|
+
@part_keys =
|
26
|
+
@simple ? [simple_key] : @parts.map { |p| p.to_name.simple_key }
|
27
|
+
end
|
28
|
+
|
29
|
+
def left
|
30
|
+
@left ||= simple? ? nil : parts[0..-2] * self.class.joint
|
31
|
+
end
|
32
|
+
|
33
|
+
def right
|
34
|
+
@right ||= simple? ? nil : parts[-1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def left_name
|
38
|
+
@left_name ||= left && self.class.new(left)
|
39
|
+
end
|
40
|
+
|
41
|
+
def right_name
|
42
|
+
@right_name ||= right && self.class.new(right)
|
43
|
+
end
|
44
|
+
|
45
|
+
def left_key
|
46
|
+
@left_key ||= simple? ? nil : part_keys[0..-2] * self.class.joint
|
47
|
+
end
|
48
|
+
|
49
|
+
def right_key
|
50
|
+
@right_key ||= simple? ? nil : part_keys.last
|
51
|
+
end
|
52
|
+
|
53
|
+
def parents
|
54
|
+
@parents ||= junction? ? [left, right] : []
|
55
|
+
end
|
56
|
+
|
57
|
+
def parent_names
|
58
|
+
@parent_names ||= junction? ? [left_name, right_name] : []
|
59
|
+
end
|
60
|
+
|
61
|
+
def parent_keys
|
62
|
+
@parent_keys ||= junction? ? [left_key, right_key] : []
|
63
|
+
end
|
64
|
+
|
65
|
+
# Note that all names have a trunk and tag,
|
66
|
+
# but only junctions have left and right
|
67
|
+
|
68
|
+
def trunk
|
69
|
+
@trunk ||= simple? ? s : left
|
70
|
+
end
|
71
|
+
|
72
|
+
def tag
|
73
|
+
@tag ||= simple? ? s : right
|
74
|
+
end
|
75
|
+
|
76
|
+
def trunk_name
|
77
|
+
@trunk_name ||= simple? ? self : left_name
|
78
|
+
end
|
79
|
+
|
80
|
+
def tag_name
|
81
|
+
@tag_name ||= simple? ? self : right_name
|
82
|
+
end
|
83
|
+
|
84
|
+
def part_names
|
85
|
+
@part_names ||= parts.map(&:to_name)
|
86
|
+
end
|
87
|
+
|
88
|
+
def piece_names
|
89
|
+
@piece_names ||= pieces.map(&:to_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# self and all ancestors (= parts and recursive lefts)
|
93
|
+
# @example
|
94
|
+
# "A+B+C+D".to_name.pieces
|
95
|
+
# # => ["A", "B", "C", "D", "A+B", "A+B+C", "A+B+C+D"]
|
96
|
+
def pieces
|
97
|
+
@pieces ||=
|
98
|
+
if simple?
|
99
|
+
[self]
|
100
|
+
else
|
101
|
+
junction_pieces = []
|
102
|
+
parts[1..-1].inject parts[0] do |left, right|
|
103
|
+
piece = [left, right] * self.class.joint
|
104
|
+
junction_pieces << piece
|
105
|
+
piece
|
106
|
+
end
|
107
|
+
parts + junction_pieces
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# name parts can be accessed and manipulated like an array
|
112
|
+
def method_missing method, *args, &block
|
113
|
+
if parts.respond_to?(method)
|
114
|
+
self.class.new parts.send(method, *args, &block)
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def respond_to? method, include_private=false
|
121
|
+
super || parts.respond_to?(method, include_private)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class SmartName
|
2
|
+
module Predicates
|
3
|
+
# @return true if name has more than one part
|
4
|
+
def junction?
|
5
|
+
!simple?
|
6
|
+
end
|
7
|
+
|
8
|
+
def blank?
|
9
|
+
s.blank?
|
10
|
+
end
|
11
|
+
alias empty? blank?
|
12
|
+
|
13
|
+
def valid?
|
14
|
+
!parts.find do |pt|
|
15
|
+
pt.match self.class.banned_re
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return true if name starts with the same parts as `prefix`
|
20
|
+
def starts_with? prefix
|
21
|
+
start_name = prefix.to_name
|
22
|
+
start_name == self[0, start_name.length]
|
23
|
+
end
|
24
|
+
alias_method :start_with?, :starts_with?
|
25
|
+
|
26
|
+
# @return true if name ends with the same parts as `prefix`
|
27
|
+
def ends_with? postfix
|
28
|
+
end_name = postfix.to_name
|
29
|
+
end_name == self[-end_name.length..-1]
|
30
|
+
end
|
31
|
+
alias_method :end_with?, :ends_with?
|
32
|
+
|
33
|
+
# @return true if name has a chain of parts that equals `subname`
|
34
|
+
def include? subname
|
35
|
+
subkey = subname.to_name.key
|
36
|
+
key =~ /(^|#{JOINT_RE})#{Regexp.quote subkey}($|#{JOINT_RE})/
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class SmartName
|
2
|
+
module Variants
|
3
|
+
def simple_key
|
4
|
+
return "" if @s.empty?
|
5
|
+
decoded
|
6
|
+
.underscore
|
7
|
+
.gsub(/[^#{OK4KEY_RE}]+/, '_')
|
8
|
+
.split(/_+/)
|
9
|
+
.reject(&:empty?)
|
10
|
+
.map { |key| SmartName.stable_uninflect(key) }
|
11
|
+
.join('_')
|
12
|
+
end
|
13
|
+
|
14
|
+
def url_key
|
15
|
+
@url_key ||= part_names.map do |part_name|
|
16
|
+
stripped = part_name.decoded.gsub(/[^#{OK4KEY_RE}]+/, ' ').strip
|
17
|
+
stripped.gsub(/[\s\_]+/, '_')
|
18
|
+
end * self.class.joint
|
19
|
+
end
|
20
|
+
|
21
|
+
# safe to be used in HTML as id ('*' and '+' are not allowed),
|
22
|
+
# but the key is no longer unique.
|
23
|
+
# For example "A-XB" and "A+*B" have the same safe_key
|
24
|
+
def safe_key
|
25
|
+
@safe_key ||= key.tr('*', 'X').tr self.class.joint, '-'
|
26
|
+
end
|
27
|
+
|
28
|
+
def decoded
|
29
|
+
@decoded ||= s.index('&') ? HTMLEntities.new.decode(s) : s
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_sym
|
33
|
+
s.to_sym
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative "../../spec_helper"
|
3
|
+
|
4
|
+
RSpec.describe SmartName::Contextual do
|
5
|
+
describe '#to_absolute' do
|
6
|
+
it 'handles _self, _whole, _' do
|
7
|
+
expect('_self'.to_name.to_absolute('foo')).to eq('foo')
|
8
|
+
expect('_whole'.to_name.to_absolute('foo')).to eq('foo')
|
9
|
+
expect('_'.to_name.to_absolute('foo')).to eq('foo')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'handles _left' do
|
13
|
+
expect('_left+Z'.to_name.to_absolute('A+B+C')).to eq('A+B+Z')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'handles white space' do
|
17
|
+
expect('_left + Z'.to_name.to_absolute('A+B+C')).to eq('A+B+Z')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'handles _right' do
|
21
|
+
expect('_right+bang'.to_name.to_absolute('nutter+butter')).to eq('butter+bang')
|
22
|
+
expect('C+_right'.to_name.to_absolute('B+A')).to eq('C+A')
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'handles leading +' do
|
26
|
+
expect('+bug'.to_name.to_absolute('hum')).to eq('hum+bug')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'handles trailing +' do
|
30
|
+
expect('bug+'.to_name.to_absolute('tracks')).to eq('bug+tracks')
|
31
|
+
end
|
32
|
+
|
33
|
+
it "handles leading + in context" do
|
34
|
+
expect("+B".to_name.to_absolute("+A")).to eq("+A+B")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "handles leading + in context for child" do
|
38
|
+
expect("+A+B".to_name.to_absolute("+A")).to eq("+A+B")
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'handles _(numbers)' do
|
42
|
+
expect('_1'.to_name.to_absolute('A+B+C')).to eq('A')
|
43
|
+
expect('_1+_2'.to_name.to_absolute('A+B+C')).to eq('A+B')
|
44
|
+
expect('_2+_3'.to_name.to_absolute('A+B+C')).to eq('B+C')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'handles _LLR etc' do
|
48
|
+
expect('_R'.to_name.to_absolute('A+B+C+D+E')).to eq('E')
|
49
|
+
expect('_L'.to_name.to_absolute('A+B+C+D+E')).to eq('A+B+C+D')
|
50
|
+
expect('_LR'.to_name.to_absolute('A+B+C+D+E')).to eq('D')
|
51
|
+
expect('_LL'.to_name.to_absolute('A+B+C+D+E')).to eq('A+B+C')
|
52
|
+
expect('_LLR'.to_name.to_absolute('A+B+C+D+E')).to eq('C')
|
53
|
+
expect('_LLL'.to_name.to_absolute('A+B+C+D+E')).to eq('A+B')
|
54
|
+
expect('_LLLR'.to_name.to_absolute('A+B+C+D+E')).to eq('B')
|
55
|
+
expect('_LLLL'.to_name.to_absolute('A+B+C+D+E')).to eq('A')
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'mismatched requests' do
|
59
|
+
it 'returns _self for _left or _right on simple cards' do
|
60
|
+
expect('_left+Z'.to_name.to_absolute('A')).to eq('A+Z')
|
61
|
+
expect('_right+Z'.to_name.to_absolute('A')).to eq('A+Z')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'handles bogus numbers' do
|
65
|
+
expect('_1'.to_name.to_absolute('A')).to eq('A')
|
66
|
+
expect('_1+_2'.to_name.to_absolute('A')).to eq('A+A')
|
67
|
+
expect('_2+_3'.to_name.to_absolute('A')).to eq('A+A')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'handles bogus _llr requests' do
|
71
|
+
expect('_R'.to_name.to_absolute('A')).to eq('A')
|
72
|
+
expect('_L'.to_name.to_absolute('A')).to eq('A')
|
73
|
+
expect('_LR'.to_name.to_absolute('A')).to eq('A')
|
74
|
+
expect('_LL'.to_name.to_absolute('A')).to eq('A')
|
75
|
+
expect('_LLR'.to_name.to_absolute('A')).to eq('A')
|
76
|
+
expect('_LLL'.to_name.to_absolute('A')).to eq('A')
|
77
|
+
expect('_LLLR'.to_name.to_absolute('A')).to eq('A')
|
78
|
+
expect('_LLLL'.to_name.to_absolute('A')).to eq('A')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#to_show' do
|
84
|
+
it 'ignores ignorables' do
|
85
|
+
expect('you+awe'.to_name.to_show('you')).to eq('+awe')
|
86
|
+
expect('me+you+awe'.to_name.to_show('you')).to eq('me+awe') #HMMM..... what should this do?
|
87
|
+
expect('me+you+awe'.to_name.to_show('me' )).to eq('+you+awe')
|
88
|
+
expect('me+you+awe'.to_name.to_show('me','you')).to eq('+awe')
|
89
|
+
expect('me+you'.to_name.to_show('me','you')).to eq('me+you')
|
90
|
+
expect('?a?+awe'.to_name.to_show('A')).to eq('+awe')
|
91
|
+
expect('+awe'.to_name.to_show()).to eq('+awe')
|
92
|
+
expect('+awe'.to_name.to_show(nil)).to eq('+awe')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "#child_of?" do
|
97
|
+
[["A+B", "A", true],
|
98
|
+
["A+B", "B", true],
|
99
|
+
["A+B+C", "A+B", true],
|
100
|
+
["A+B+C", "C", true],
|
101
|
+
["A+B", "A+B", false],
|
102
|
+
["A+B", "A+B+C", false],
|
103
|
+
["A", "A", false],
|
104
|
+
["A+B+C", "A", false],
|
105
|
+
["A+C", "A+B", false],
|
106
|
+
["A+B", "C+B", false],
|
107
|
+
["X+A+B", "A+C", false],
|
108
|
+
["+A", "B", true],
|
109
|
+
["+A", "A", true],
|
110
|
+
["+A", "+D", true],
|
111
|
+
["+A", "+A", false]].each do |a, b, res|
|
112
|
+
it "#{a} is a child of #{b}" do
|
113
|
+
expect(a.to_name.child_of?(b)).to be res
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#relative_name" do
|
119
|
+
[["A+B", "A", "+B"],
|
120
|
+
["A+B", "B", "A"],
|
121
|
+
["A", "A", "A"],
|
122
|
+
["A+B", "A+B", "A+B"],
|
123
|
+
["A", "A+B", "A"],
|
124
|
+
["A+C", "A+B", "+C"],
|
125
|
+
["A+B", "C+B", "A"],
|
126
|
+
["X+A+B", "A+C", "X+B"]].each do |name, context, res|
|
127
|
+
it "#{name} relative to #{context} is #{res}" do
|
128
|
+
expect(name.to_name.relative_name(context).to_s).to eq res
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative "../../spec_helper"
|
3
|
+
|
4
|
+
RSpec.describe SmartName::Manipulate do
|
5
|
+
describe '#replace' do
|
6
|
+
def replace str, change
|
7
|
+
str.to_name.replace(*change.to_a.flatten).to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'replaces first name part' do
|
11
|
+
expect(replace 'a+b', 'a' => 'x').to eq('x+b')
|
12
|
+
end
|
13
|
+
it 'replaces last name part' do
|
14
|
+
expect(replace 'a+b', 'b' => 'x').to eq('a+x')
|
15
|
+
end
|
16
|
+
it 'replaces middle name part' do
|
17
|
+
expect(replace 'a+c+b', 'c' => 'x').to eq('a+x+b')
|
18
|
+
end
|
19
|
+
it 'replaces all occurences' do
|
20
|
+
expect(replace'a+c+b+c+c','c' => 'x').to eq('a+x+b+x+x')
|
21
|
+
end
|
22
|
+
it 'replaces junction' do
|
23
|
+
expect(replace'a+b+c', 'a+b' => 'x').to eq('x+c')
|
24
|
+
expect(replace'a+b+c+d', 'a+b' => 'e+f').to eq('e+f+c+d')
|
25
|
+
end
|
26
|
+
it "replaces two part tag" do
|
27
|
+
expect(replace'a+b+c','b+c' => 'x').to eq('a+x')
|
28
|
+
end
|
29
|
+
it "replaces whole name" do
|
30
|
+
expect(replace'a+b+c','a+b+c' => 'x').to eq('x')
|
31
|
+
end
|
32
|
+
|
33
|
+
it "replaces based on key match" do
|
34
|
+
expect(replace'A+ b +C?','a+b+c' => 'x').to eq('x')
|
35
|
+
end
|
36
|
+
it "replaces with original format" do
|
37
|
+
expect(replace'a+b','a+B' => 'X?+C').to eq('X?+C')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#replace_part' do
|
42
|
+
def replace str, change
|
43
|
+
str.to_name.replace_part(*change.to_a.flatten).to_s
|
44
|
+
end
|
45
|
+
it 'replaces all occurences' do
|
46
|
+
expect(replace'a+c+b+c+c','c' => 'x').to eq('a+x+b+x+x')
|
47
|
+
end
|
48
|
+
context "first argument is not a part" do
|
49
|
+
it 'raises error' do
|
50
|
+
expect { replace('a+c+b','a+c' => 'x') }.to raise_error(StandardError, /has to be simple/)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#replace_piece' do
|
56
|
+
def replace str, change
|
57
|
+
str.to_name.replace_piece(*change.to_a.flatten).to_s
|
58
|
+
end
|
59
|
+
it "replaces two part trunk" do
|
60
|
+
expect(replace('a+b+c', 'a+b' =>'x')).to eq('x+c')
|
61
|
+
end
|
62
|
+
it "doesn't replace two part tag" do
|
63
|
+
expect(replace('a+b+c', 'b+c' => 'x')).to eq('a+b+c')
|
64
|
+
end
|
65
|
+
it "replaces whole name" do
|
66
|
+
expect(replace'a+b+c','a+b+c' => 'x').to eq('x')
|
67
|
+
end
|
68
|
+
it "replaces based on key match" do
|
69
|
+
expect(replace'A+ b +C?','a+b+c' => 'x').to eq('x')
|
70
|
+
end
|
71
|
+
it "replaces with original format" do
|
72
|
+
expect(replace'a+b','a+B' => 'X?+C').to eq('X?+C')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|