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.
@@ -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