smartname 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|