strokedb 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CONTRIBUTORS +7 -0
- data/CREDITS +13 -0
- data/README +44 -0
- data/bin/sdbc +2 -0
- data/lib/config/config.rb +161 -0
- data/lib/data_structures/inverted_list.rb +297 -0
- data/lib/data_structures/point_query.rb +24 -0
- data/lib/data_structures/skiplist.rb +302 -0
- data/lib/document/associations.rb +107 -0
- data/lib/document/callback.rb +11 -0
- data/lib/document/coercions.rb +57 -0
- data/lib/document/delete.rb +28 -0
- data/lib/document/document.rb +684 -0
- data/lib/document/meta.rb +261 -0
- data/lib/document/slot.rb +199 -0
- data/lib/document/util.rb +27 -0
- data/lib/document/validations.rb +704 -0
- data/lib/document/versions.rb +106 -0
- data/lib/document/virtualize.rb +82 -0
- data/lib/init.rb +57 -0
- data/lib/stores/chainable_storage.rb +57 -0
- data/lib/stores/inverted_list_index/inverted_list_file_storage.rb +56 -0
- data/lib/stores/inverted_list_index/inverted_list_index.rb +49 -0
- data/lib/stores/remote_store.rb +172 -0
- data/lib/stores/skiplist_store/chunk.rb +119 -0
- data/lib/stores/skiplist_store/chunk_storage.rb +21 -0
- data/lib/stores/skiplist_store/file_chunk_storage.rb +44 -0
- data/lib/stores/skiplist_store/memory_chunk_storage.rb +37 -0
- data/lib/stores/skiplist_store/skiplist_store.rb +217 -0
- data/lib/stores/store.rb +5 -0
- data/lib/sync/chain_sync.rb +38 -0
- data/lib/sync/diff.rb +126 -0
- data/lib/sync/lamport_timestamp.rb +81 -0
- data/lib/sync/store_sync.rb +79 -0
- data/lib/sync/stroke_diff/array.rb +102 -0
- data/lib/sync/stroke_diff/default.rb +21 -0
- data/lib/sync/stroke_diff/hash.rb +186 -0
- data/lib/sync/stroke_diff/string.rb +116 -0
- data/lib/sync/stroke_diff/stroke_diff.rb +9 -0
- data/lib/util/blankslate.rb +42 -0
- data/lib/util/ext/blank.rb +50 -0
- data/lib/util/ext/enumerable.rb +36 -0
- data/lib/util/ext/fixnum.rb +16 -0
- data/lib/util/ext/hash.rb +22 -0
- data/lib/util/ext/object.rb +8 -0
- data/lib/util/ext/string.rb +35 -0
- data/lib/util/inflect.rb +217 -0
- data/lib/util/java_util.rb +9 -0
- data/lib/util/lazy_array.rb +54 -0
- data/lib/util/lazy_mapping_array.rb +64 -0
- data/lib/util/lazy_mapping_hash.rb +46 -0
- data/lib/util/serialization.rb +29 -0
- data/lib/util/trigger_partition.rb +136 -0
- data/lib/util/util.rb +38 -0
- data/lib/util/xml.rb +6 -0
- data/lib/view/view.rb +55 -0
- data/script/console +70 -0
- data/strokedb.rb +75 -0
- metadata +148 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
module StrokeDB
|
2
|
+
class ::Array
|
3
|
+
SDATPTAGS = {
|
4
|
+
'-' => PATCH_MINUS,
|
5
|
+
'+' => PATCH_PLUS,
|
6
|
+
'!' => PATCH_DIFF
|
7
|
+
}.freeze
|
8
|
+
def stroke_diff(to)
|
9
|
+
return super(to) unless Array === to
|
10
|
+
return nil if self == to
|
11
|
+
|
12
|
+
# sdiff: + - ! =
|
13
|
+
lcs_sdiff = ::Diff::LCS.sdiff(self, to)
|
14
|
+
patchset = lcs_sdiff.inject([]) do |patchset, change|
|
15
|
+
a = SDATPTAGS[change.action]
|
16
|
+
if a == PATCH_DIFF
|
17
|
+
patchset << [a, change.new_position, change.old_element.stroke_diff(change.new_element)]
|
18
|
+
elsif a == PATCH_MINUS
|
19
|
+
patchset << [a, change.new_position]
|
20
|
+
elsif a == PATCH_PLUS
|
21
|
+
patchset << [a, change.new_position, change.new_element]
|
22
|
+
end
|
23
|
+
patchset
|
24
|
+
end
|
25
|
+
patchset.empty? ? nil : patchset
|
26
|
+
end
|
27
|
+
|
28
|
+
def stroke_patch(patch)
|
29
|
+
return self unless patch
|
30
|
+
return patch[1] if patch[0] == PATCH_REPLACE
|
31
|
+
res = self.dup
|
32
|
+
patch.each do |change|
|
33
|
+
_stroke_elementary_patch(res, change[1], change)
|
34
|
+
end
|
35
|
+
res
|
36
|
+
end
|
37
|
+
|
38
|
+
def stroke_merge(patch1, patch2)
|
39
|
+
unless patch1 && patch2
|
40
|
+
return _stroke_automerged(stroke_patch(patch1 || patch2))
|
41
|
+
end
|
42
|
+
|
43
|
+
patch1 = patch1.dup
|
44
|
+
patch2 = patch2.dup
|
45
|
+
|
46
|
+
c1 = patch1.shift
|
47
|
+
c2 = patch2.shift
|
48
|
+
|
49
|
+
offset1 = 0
|
50
|
+
offset2 = 0
|
51
|
+
result = self.dup
|
52
|
+
result1 = nil
|
53
|
+
result2 = nil
|
54
|
+
|
55
|
+
while c1 && c2
|
56
|
+
while c1 && (p1 = c1[1] + offset1) && (p2 = c2[1] + offset2) && p1 < p2
|
57
|
+
offset2 += _stroke_elementary_patch(result, p1, c1)
|
58
|
+
c1 = patch1.shift
|
59
|
+
end
|
60
|
+
|
61
|
+
if p1 == p2
|
62
|
+
raise "TODO conflict resolution!"
|
63
|
+
|
64
|
+
c1 = patch1.shift
|
65
|
+
end
|
66
|
+
|
67
|
+
while c1 && c2 && (p1 = c1[1] + offset1) && (p2 = c2[1] + offset2) && p2 < p1
|
68
|
+
offset1 += _stroke_elementary_patch(result, p2, c2)
|
69
|
+
c2 = patch2.shift
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Tail (one of two)
|
74
|
+
while c1
|
75
|
+
offset2 += _stroke_elementary_patch(result, c1[1] + offset1, c1)
|
76
|
+
c1 = patch1.shift
|
77
|
+
end
|
78
|
+
while c2
|
79
|
+
offset1 += _stroke_elementary_patch(result, c2[1] + offset2, c2)
|
80
|
+
c2 = patch2.shift
|
81
|
+
end
|
82
|
+
result ? _stroke_automerged(result) : _stroke_conflicted(result1, result2)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
def _stroke_elementary_patch(result, pos, change)
|
87
|
+
a = change[0]
|
88
|
+
case a
|
89
|
+
when PATCH_MINUS
|
90
|
+
result.delete_at(pos)
|
91
|
+
-1
|
92
|
+
when PATCH_PLUS
|
93
|
+
result[pos, 0] = [change[2]]
|
94
|
+
+1
|
95
|
+
when PATCH_DIFF
|
96
|
+
result[pos] = result[pos].stroke_patch(change[2])
|
97
|
+
0
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module StrokeDB
|
2
|
+
class ::Object
|
3
|
+
def stroke_diff(to)
|
4
|
+
self == to ? nil : [PATCH_REPLACE, to]
|
5
|
+
end
|
6
|
+
def stroke_patch(patch)
|
7
|
+
patch ? patch[1] : self
|
8
|
+
end
|
9
|
+
def stroke_merge(patch1, patch2) # => is_conflict, result1, result2
|
10
|
+
r1 = self.stroke_patch(patch1)
|
11
|
+
r2 = self.stroke_patch(patch2)
|
12
|
+
[r1 != r2, r1, r2]
|
13
|
+
end
|
14
|
+
def _stroke_automerged(r)
|
15
|
+
[false, r, r]
|
16
|
+
end
|
17
|
+
def _stroke_conflicted(r1, r2)
|
18
|
+
[true, r1, r2]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module StrokeDB
|
2
|
+
class ::Hash
|
3
|
+
def stroke_diff(to)
|
4
|
+
return super(to) unless Hash === to
|
5
|
+
return nil if self == to
|
6
|
+
|
7
|
+
all_keys = self.keys | to.keys
|
8
|
+
|
9
|
+
deleted_slots = []
|
10
|
+
inserted_slots = {}
|
11
|
+
diffed_slots = {}
|
12
|
+
|
13
|
+
all_keys.each do |k|
|
14
|
+
unless to.key?(k)
|
15
|
+
deleted_slots << k
|
16
|
+
else
|
17
|
+
unless self.key?(k)
|
18
|
+
inserted_slots[k] = to[k]
|
19
|
+
else
|
20
|
+
diff = self[k].stroke_diff(to[k])
|
21
|
+
diffed_slots[k] = diff if diff
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
[deleted_slots, inserted_slots, diffed_slots]
|
26
|
+
end
|
27
|
+
|
28
|
+
def stroke_patch(patch)
|
29
|
+
return self unless patch
|
30
|
+
return patch[1] if patch[0] == PATCH_REPLACE
|
31
|
+
res = self.dup
|
32
|
+
deleted_slots, inserted_slots, diffed_slots = patch
|
33
|
+
deleted_slots.each {|k| res.delete(k) }
|
34
|
+
res.merge!(inserted_slots)
|
35
|
+
diffed_slots.each {|k,v| res[k] = self[k].stroke_patch(v) }
|
36
|
+
res
|
37
|
+
end
|
38
|
+
|
39
|
+
# Hash conflict may occur:
|
40
|
+
# 1) If key accures in conflicting groups
|
41
|
+
# (e.g. deleted v.s. inserted, deleted vs. diffed)
|
42
|
+
# 2) If slot diff yields conflict
|
43
|
+
#
|
44
|
+
def stroke_merge(patch1, patch2)
|
45
|
+
unless patch1 && patch2
|
46
|
+
return _stroke_automerged(stroke_patch(patch1 || patch2))
|
47
|
+
end
|
48
|
+
|
49
|
+
del1, ins1, dif1 = patch1[0].dup, patch1[1].dup, patch1[2].dup
|
50
|
+
del2, ins2, dif2 = patch2[0].dup, patch2[1].dup, patch2[2].dup
|
51
|
+
|
52
|
+
# TEMPORARY: inconsistency check
|
53
|
+
conflict_d1i2 = del1 & ins2.keys
|
54
|
+
conflict_d2i1 = del2 & ins1.keys
|
55
|
+
conflict_i1f2 = ins1.keys & dif2.keys
|
56
|
+
conflict_i2f1 = ins2.keys & dif1.keys
|
57
|
+
|
58
|
+
unless conflict_d1i2.empty? && conflict_d2i1.empty? &&
|
59
|
+
conflict_i1f2.empty? && conflict_i2f1.empty?
|
60
|
+
raise "Fatal inconsistency on stroke_merge detected!"
|
61
|
+
end
|
62
|
+
|
63
|
+
overlapping_keys = ((del1 + ins1.keys + dif1.keys) &
|
64
|
+
(del2 + ins2.keys + dif2.keys))
|
65
|
+
# make hash for faster inclusion tests
|
66
|
+
overlapping_keys_hash = overlapping_keys.inject({}) do |h, k|
|
67
|
+
h[k] = 1; h
|
68
|
+
end
|
69
|
+
|
70
|
+
result = self.dup
|
71
|
+
|
72
|
+
# 1. Merge non-overlapping updates
|
73
|
+
(del1 + del2 - overlapping_keys).each do |k|
|
74
|
+
del1.delete(k)
|
75
|
+
del2.delete(k)
|
76
|
+
result.delete(k)
|
77
|
+
end
|
78
|
+
ins1.dup.each do |k, v|
|
79
|
+
unless overlapping_keys_hash[k]
|
80
|
+
result[k] = v
|
81
|
+
ins1.delete(k)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
ins2.dup.each do |k, v|
|
85
|
+
unless overlapping_keys_hash[k]
|
86
|
+
result[k] = v
|
87
|
+
ins2.delete(k)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
dif1.dup.each do |k, diff|
|
91
|
+
unless overlapping_keys_hash[k]
|
92
|
+
result[k] = stroke_patch(diff)
|
93
|
+
dif1.delete(k)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
dif2.dup.each do |k, diff|
|
97
|
+
unless overlapping_keys_hash[k]
|
98
|
+
result[k] = stroke_patch(diff)
|
99
|
+
dif2.delete(k)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# 2. Resolve overlapping keys
|
104
|
+
#
|
105
|
+
# Overlapping key may be in such pairs:
|
106
|
+
#
|
107
|
+
# [dif, dif] <- possible conflict
|
108
|
+
# [dif, ins] <- anomaly
|
109
|
+
# [ins, ins] <- possible conflict
|
110
|
+
# [del, ins] <- anomaly
|
111
|
+
# [del, dif] <- conflict
|
112
|
+
# [del, del] <- not a conflict
|
113
|
+
#
|
114
|
+
# (and in reverse order as well)
|
115
|
+
|
116
|
+
result1 = nil
|
117
|
+
result2 = nil
|
118
|
+
del1.each do |k|
|
119
|
+
if ins2.key?(k)
|
120
|
+
raise "Fatal inconsistency on stroke_merge detected: delete + insert"
|
121
|
+
elsif dif2.key?(k)
|
122
|
+
# Conflict. Split result if not splitted.
|
123
|
+
result1, result2, result = _stroke_split_merge_result(result) if result
|
124
|
+
result1.delete(k)
|
125
|
+
result2[k] = result2[k].stroke_patch(dif2[k])
|
126
|
+
else # [del, del]
|
127
|
+
if result
|
128
|
+
result.delete(k)
|
129
|
+
else
|
130
|
+
result1.delete(k)
|
131
|
+
result2.delete(k)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
dif1.each do |k, diff|
|
136
|
+
if ins2.key?(k)
|
137
|
+
raise "Fatal inconsistency on stroke_merge detected: diff + insert"
|
138
|
+
elsif dif2.key?(k) # possible conflict
|
139
|
+
conflict, r1, r2 = self[k].stroke_merge(diff, dif2[k])
|
140
|
+
if conflict
|
141
|
+
result1, result2, result = _stroke_split_merge_result(result) if result
|
142
|
+
result1[k] = r1
|
143
|
+
result2[k] = r2
|
144
|
+
else
|
145
|
+
if result
|
146
|
+
result[k] = r2
|
147
|
+
else
|
148
|
+
result1[k] = r2
|
149
|
+
result2[k] = r2
|
150
|
+
end
|
151
|
+
end
|
152
|
+
else # [dif, del] <- conflict
|
153
|
+
# Conflict. Split result if not splitted.
|
154
|
+
result1, result2, result = _stroke_split_merge_result(result) if result
|
155
|
+
result1[k] = result1[k].stroke_patch(diff)
|
156
|
+
result2.delete(k)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
ins1.each do |k, obj|
|
160
|
+
if ins2.key?(k) # possible conflict
|
161
|
+
if obj != ins2[k]
|
162
|
+
result1, result2, result = _stroke_split_merge_result(result) if result
|
163
|
+
result1[k] = obj
|
164
|
+
result2[k] = ins2[k]
|
165
|
+
else
|
166
|
+
if result
|
167
|
+
result[k] = obj
|
168
|
+
else
|
169
|
+
result1[k] = obj
|
170
|
+
result2[k] = obj
|
171
|
+
end
|
172
|
+
end
|
173
|
+
else # delete or diff
|
174
|
+
raise "Fatal inconsistency on stroke_merge detected: insert + (delete|diff)"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
result ? _stroke_automerged(result) : _stroke_conflicted(result1, result2)
|
179
|
+
end
|
180
|
+
|
181
|
+
# In case of conflict, result is copied to result{1,2} and nullified.
|
182
|
+
def _stroke_split_merge_result(result)
|
183
|
+
return [result.dup, result.dup, nil]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module StrokeDB
|
2
|
+
class ::String
|
3
|
+
def stroke_diff(to)
|
4
|
+
return super(to) unless String === to
|
5
|
+
return nil if self == to
|
6
|
+
|
7
|
+
_f = self[0,2]
|
8
|
+
_t = to[0,2]
|
9
|
+
pfx = "@#"
|
10
|
+
# both are refs
|
11
|
+
return super(to) if _f == _t && _t == pfx
|
12
|
+
# one of items is ref, another is not.
|
13
|
+
return super(to) if _f == pfx || _t == pfx
|
14
|
+
|
15
|
+
lcs_diff = ::Diff::LCS.diff(self, to)
|
16
|
+
patchset = lcs_diff.map do |changes|
|
17
|
+
parts = []
|
18
|
+
last_part = changes.inject(nil) do |part, change|
|
19
|
+
if part && part[0] == change.action && part[3] == change.position - 1
|
20
|
+
part[3] += 1
|
21
|
+
part[2] << change.element
|
22
|
+
part
|
23
|
+
else
|
24
|
+
parts << part if part
|
25
|
+
# emit
|
26
|
+
[change.action, change.position, change.element, change.position]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
parts << last_part if last_part
|
30
|
+
parts.empty? ? nil : parts
|
31
|
+
end.compact.inject([]) do |patches, ps|
|
32
|
+
ps.map do |p|
|
33
|
+
patches << if p[0] == '+'
|
34
|
+
[PATCH_PLUS, p[1], p[2]] # [+ position_in_b substr]
|
35
|
+
else
|
36
|
+
[PATCH_MINUS, p[1], p[2].size] # [- position_in_a length]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
patches
|
40
|
+
end
|
41
|
+
#p patchset
|
42
|
+
patchset.empty? ? nil : patchset
|
43
|
+
end
|
44
|
+
|
45
|
+
def stroke_patch(patch)
|
46
|
+
return self unless patch
|
47
|
+
return patch[1] if patch[0] == PATCH_REPLACE
|
48
|
+
|
49
|
+
# Patch is a list of insertions and deletions.
|
50
|
+
# Deletion is indexed relative to base.
|
51
|
+
# Insertion is indexed relative to new string.
|
52
|
+
res = ""
|
53
|
+
ai = bj = 0
|
54
|
+
patch.each do |change|
|
55
|
+
action, position, element = change
|
56
|
+
case action
|
57
|
+
when PATCH_MINUS
|
58
|
+
d = position - ai
|
59
|
+
if d > 0
|
60
|
+
res << self[ai, d]
|
61
|
+
ai += d
|
62
|
+
bj += d
|
63
|
+
end
|
64
|
+
ai += element # element == length
|
65
|
+
when PATCH_PLUS
|
66
|
+
d = position - bj
|
67
|
+
if d > 0
|
68
|
+
res << self[ai, d]
|
69
|
+
ai += d
|
70
|
+
bj += d
|
71
|
+
end
|
72
|
+
bj += element.size
|
73
|
+
res << element
|
74
|
+
end
|
75
|
+
end
|
76
|
+
d = self.size - ai
|
77
|
+
res << self[ai, d] if d > 0
|
78
|
+
res
|
79
|
+
end
|
80
|
+
|
81
|
+
def stroke_merge(patch1, patch2)
|
82
|
+
# One patch is missing (i.e. no changes)
|
83
|
+
unless patch1 && patch2
|
84
|
+
return _stroke_automerged(stroke_patch(patch1 || patch2))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Patch could be either PATCH_REPLACE or regular string diff.
|
88
|
+
# Thus, 4 cases:
|
89
|
+
#
|
90
|
+
# [replace, replace] -> possible conflict
|
91
|
+
# [replace, diff] -> conflict
|
92
|
+
# [diff, replace] -> conflict
|
93
|
+
# [diff, diff] -> possible conflict
|
94
|
+
|
95
|
+
# Code is verbose to be fast and clear
|
96
|
+
if patch1[0] == PATCH_REPLACE
|
97
|
+
if patch2[0] == PATCH_REPLACE # [replace, replace]
|
98
|
+
if patch1[1] != patch2[1]
|
99
|
+
return _stroke_conflicted(stroke_patch(patch1), stroke_patch(patch2))
|
100
|
+
else
|
101
|
+
return _stroke_automerged(stroke_patch(patch1))
|
102
|
+
end
|
103
|
+
else # [replace, diff]
|
104
|
+
return _stroke_conflicted(stroke_patch(patch1), stroke_patch(patch2))
|
105
|
+
end
|
106
|
+
else
|
107
|
+
if patch1[0] == PATCH_REPLACE # [diff, replace]
|
108
|
+
return _stroke_conflicted(stroke_patch(patch1), stroke_patch(patch2))
|
109
|
+
else
|
110
|
+
nil # [diff, diff] - see below
|
111
|
+
end
|
112
|
+
end
|
113
|
+
# TODO: ...
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
unless defined?(BlankSlate)
|
2
|
+
class BlankSlate < BasicObject; end if defined?(BasicObject)
|
3
|
+
|
4
|
+
class BlankSlate
|
5
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class BlankSlate
|
10
|
+
MethodMapping = {
|
11
|
+
'[]' => 'squarebracket',
|
12
|
+
'[]=' => 'squarebracket_set',
|
13
|
+
'<<' => 'leftarrow',
|
14
|
+
'*' => 'star',
|
15
|
+
'+' => 'plus',
|
16
|
+
'-' => 'minus',
|
17
|
+
'&' => 'bitwiseand',
|
18
|
+
'|' => 'bitwiseor',
|
19
|
+
'<=>' => 'spaceship',
|
20
|
+
'==' => 'equalequal',
|
21
|
+
'===' => 'tripleequal',
|
22
|
+
'=~' => 'regexmatch',
|
23
|
+
'`' => 'backtick',
|
24
|
+
} unless defined? MethodMapping
|
25
|
+
end
|
26
|
+
|
27
|
+
def BlankSlate superclass = nil
|
28
|
+
if superclass
|
29
|
+
(@blank_slates ||= {})[superclass] ||= Class.new(superclass) do
|
30
|
+
instance_methods.sort.each { |m|
|
31
|
+
unless m =~ /^__/
|
32
|
+
mname = "__#{::BlankSlate::MethodMapping[m.to_s] || m}"
|
33
|
+
class_eval "alias :'#{mname}' :'#{m}'"
|
34
|
+
|
35
|
+
undef_method m
|
36
|
+
end
|
37
|
+
}
|
38
|
+
end
|
39
|
+
else
|
40
|
+
BlankSlate
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Object
|
2
|
+
# An object is blank if it's nil, empty, or a whitespace string.
|
3
|
+
# For example, "", " ", nil, [], and {} are blank.
|
4
|
+
#
|
5
|
+
# This simplifies
|
6
|
+
# if !address.nil? && !address.empty?
|
7
|
+
# to
|
8
|
+
# if !address.blank?
|
9
|
+
def blank?
|
10
|
+
respond_to?(:empty?) ? empty? : !self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class NilClass #:nodoc:
|
15
|
+
def blank?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class FalseClass #:nodoc:
|
21
|
+
def blank?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class TrueClass #:nodoc:
|
27
|
+
def blank?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Array #:nodoc:
|
33
|
+
alias_method :blank?, :empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
class Hash #:nodoc:
|
37
|
+
alias_method :blank?, :empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
class String #:nodoc:
|
41
|
+
def blank?
|
42
|
+
self !~ /\S/
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Numeric #:nodoc:
|
47
|
+
def blank?
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# extracted from ActiveRecord (http://rubyforge.org/projects/activesupport/)
|
2
|
+
|
3
|
+
module Enumerable
|
4
|
+
# Collect an enumerable into sets, grouped by the result of a block. Useful,
|
5
|
+
# for example, for grouping records by date.
|
6
|
+
def group_by
|
7
|
+
inject({}) do |groups, element|
|
8
|
+
(groups[yield(element)] ||= []) << element
|
9
|
+
groups
|
10
|
+
end
|
11
|
+
end if RUBY_VERSION < '1.9'
|
12
|
+
|
13
|
+
# Map and each_with_index combined.
|
14
|
+
def map_with_index
|
15
|
+
collected=[]
|
16
|
+
each_with_index {|item, index| collected << yield(item, index) }
|
17
|
+
collected
|
18
|
+
end
|
19
|
+
|
20
|
+
alias :collect_with_index :map_with_index
|
21
|
+
|
22
|
+
def each_consecutive_pair
|
23
|
+
first = true
|
24
|
+
prev = nil
|
25
|
+
|
26
|
+
each do |val|
|
27
|
+
unless first
|
28
|
+
yield prev, val
|
29
|
+
else
|
30
|
+
first = false
|
31
|
+
end
|
32
|
+
|
33
|
+
prev = val
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# extracted and adapted from ActiveRecord (http://rubyforge.org/projects/activesupport/)
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
def stringify_keys
|
5
|
+
inject({}) do |options, (key, value)|
|
6
|
+
options[key.to_s] = value
|
7
|
+
options
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def except(*keys)
|
12
|
+
reject { |key,| keys.include?(key.to_sym) or keys.include?(key.to_s) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def reverse_merge(other_hash)
|
16
|
+
other_hash.merge(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reverse_merge!(other_hash)
|
20
|
+
replace(reverse_merge(other_hash))
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# extracted and adapted from ActiveRecord (http://rubyforge.org/projects/activesupport/)
|
2
|
+
|
3
|
+
class String
|
4
|
+
def ends_with?(suffix)
|
5
|
+
suffix = suffix.to_s
|
6
|
+
self[-suffix.length, suffix.length] == suffix
|
7
|
+
end
|
8
|
+
|
9
|
+
def camel_case
|
10
|
+
split('_').map{|e| e.capitalize}.join
|
11
|
+
end
|
12
|
+
alias :camelize :camel_case
|
13
|
+
|
14
|
+
def snake_case
|
15
|
+
gsub(/\B[A-Z][^A-Z]/, '_\&').downcase.gsub(' ', '_')
|
16
|
+
end
|
17
|
+
|
18
|
+
def tableize
|
19
|
+
words = snake_case.split('_')
|
20
|
+
words.last.replace words.last.plural
|
21
|
+
words.join('_')
|
22
|
+
end
|
23
|
+
|
24
|
+
def demodulize
|
25
|
+
gsub(/^.*::/, '')
|
26
|
+
end
|
27
|
+
|
28
|
+
def constantize
|
29
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
|
30
|
+
raise NameError, "#{self.inspect} is not a valid constant name!"
|
31
|
+
end
|
32
|
+
|
33
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
34
|
+
end
|
35
|
+
end
|