friends 0.16 → 0.17
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/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +59 -2
- data/bin/friends +74 -18
- data/friends.md +9 -4
- data/lib/friends/activity.rb +101 -42
- data/lib/friends/friend.rb +15 -39
- data/lib/friends/introvert.rb +229 -71
- data/lib/friends/location.rb +49 -0
- data/lib/friends/regex_builder.rb +38 -0
- data/lib/friends/version.rb +1 -1
- data/test/activity_spec.rb +89 -18
- data/test/friend_spec.rb +2 -13
- data/test/introvert_spec.rb +330 -56
- data/test/location_spec.rb +58 -0
- metadata +6 -2
data/lib/friends/friend.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# Friend represents a friend. You know, a real-life friend!
|
3
3
|
|
4
|
+
require "friends/regex_builder"
|
4
5
|
require "friends/serializable"
|
5
6
|
|
6
7
|
module Friends
|
@@ -14,7 +15,7 @@ module Friends
|
|
14
15
|
def self.deserialization_regex
|
15
16
|
# Note: this regex must be on one line because whitespace is important
|
16
17
|
# rubocop:disable Metrics/LineLength
|
17
|
-
/(#{SERIALIZATION_PREFIX})?(?<name>[^\(]+)(\((?<nickname_str>#{NICKNAME_PREFIX}.+)\))?/
|
18
|
+
/(#{SERIALIZATION_PREFIX})?(?<name>[^\(\[]+)(\((?<nickname_str>#{NICKNAME_PREFIX}.+)\))?\s?(\[(?<location_name>[^\]]+)\])?/
|
18
19
|
# rubocop:enable Metrics/LineLength
|
19
20
|
end
|
20
21
|
|
@@ -24,14 +25,16 @@ module Friends
|
|
24
25
|
end
|
25
26
|
|
26
27
|
# @param name [String] the name of the friend
|
27
|
-
def initialize(name:, nickname_str: nil)
|
28
|
+
def initialize(name:, nickname_str: nil, location_name: nil)
|
28
29
|
@name = name.strip
|
29
30
|
@nicknames = nickname_str &&
|
30
31
|
nickname_str.split(NICKNAME_PREFIX)[1..-1].map(&:strip) ||
|
31
32
|
[]
|
33
|
+
@location_name = location_name
|
32
34
|
end
|
33
35
|
|
34
36
|
attr_accessor :name
|
37
|
+
attr_accessor :location_name
|
35
38
|
|
36
39
|
# @return [String] the file serialization text for the friend
|
37
40
|
def serialize
|
@@ -40,10 +43,16 @@ module Friends
|
|
40
43
|
|
41
44
|
# @return [String] a string representing the friend's name and nicknames
|
42
45
|
def to_s
|
43
|
-
|
46
|
+
unless @nicknames.empty?
|
47
|
+
nickname_str =
|
48
|
+
" (" +
|
49
|
+
@nicknames.map { |n| "#{NICKNAME_PREFIX}#{n}" }.join(" ") +
|
50
|
+
")"
|
51
|
+
end
|
52
|
+
|
53
|
+
location_str = " [#{@location_name}]" unless @location_name.nil?
|
44
54
|
|
45
|
-
|
46
|
-
"#{name} (#{nickname_str})"
|
55
|
+
"#{@name}#{nickname_str}#{location_str}"
|
47
56
|
end
|
48
57
|
|
49
58
|
# Adds a nickname, avoiding duplicates and stripping surrounding whitespace.
|
@@ -53,13 +62,6 @@ module Friends
|
|
53
62
|
@nicknames.uniq!
|
54
63
|
end
|
55
64
|
|
56
|
-
# Renames a friend, avoiding duplicates and stripping surrounding
|
57
|
-
# whitespace.
|
58
|
-
# @param new_name [String] the friend's new name
|
59
|
-
def rename(new_name)
|
60
|
-
@name = new_name
|
61
|
-
end
|
62
|
-
|
63
65
|
# @param nickname [String] the nickname to remove
|
64
66
|
# @return [Boolean] true if the nickname was present, false otherwise
|
65
67
|
def remove_nickname(nickname)
|
@@ -97,36 +99,10 @@ module Friends
|
|
97
99
|
# We generously allow any amount of whitespace between parts of a name.
|
98
100
|
splitter = "\\s+"
|
99
101
|
|
100
|
-
# We don't want to match names that are "escaped" with a leading
|
101
|
-
# backslash.
|
102
|
-
no_leading_backslash = "(?<!\\\\)"
|
103
|
-
|
104
|
-
# We don't want to match names that are directly touching double asterisks
|
105
|
-
# as these are treated as sacred by our program.
|
106
|
-
# NOTE: Technically we don't need this check here, since we perform a more
|
107
|
-
# complex asterisk check in the Activity#description_matches method, but
|
108
|
-
# this class shouldn't need to know about the internal implementation of
|
109
|
-
# another class.
|
110
|
-
no_leading_asterisks = "(?<!\\*\\*)"
|
111
|
-
no_ending_asterisks = "(?!\\*\\*)"
|
112
|
-
|
113
|
-
# We don't want to match names that are part of other words.
|
114
|
-
no_leading_alphabeticals = "(?<![A-z])"
|
115
|
-
no_ending_alphabeticals = "(?![A-z])"
|
116
|
-
|
117
102
|
# Create the list of regexes and return it.
|
118
103
|
chunks = name.split(Regexp.new(splitter))
|
119
|
-
|
120
104
|
[chunks, [chunks.first], *@nicknames.map { |n| [n] }].map do |words|
|
121
|
-
|
122
|
-
no_leading_backslash +
|
123
|
-
no_leading_asterisks +
|
124
|
-
no_leading_alphabeticals +
|
125
|
-
words.join(splitter) +
|
126
|
-
no_ending_alphabeticals +
|
127
|
-
no_ending_asterisks,
|
128
|
-
Regexp::IGNORECASE
|
129
|
-
)
|
105
|
+
Friends::RegexBuilder.regex(words.join(splitter))
|
130
106
|
end
|
131
107
|
end
|
132
108
|
|
data/lib/friends/introvert.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
|
6
6
|
require "friends/activity"
|
7
7
|
require "friends/friend"
|
8
|
+
require "friends/location"
|
8
9
|
require "friends/friends_error"
|
9
10
|
|
10
11
|
module Friends
|
@@ -12,6 +13,7 @@ module Friends
|
|
12
13
|
DEFAULT_FILENAME = "./friends.md".freeze
|
13
14
|
ACTIVITIES_HEADER = "### Activities:".freeze
|
14
15
|
FRIENDS_HEADER = "### Friends:".freeze
|
16
|
+
LOCATIONS_HEADER = "### Locations:".freeze
|
15
17
|
GRAPH_DATE_FORMAT = "%b %Y".freeze # Used as the param for date.strftime().
|
16
18
|
|
17
19
|
# @param filename [String] the name of the friends Markdown file
|
@@ -25,22 +27,21 @@ module Friends
|
|
25
27
|
|
26
28
|
# Write out the friends file with cleaned/sorted data.
|
27
29
|
def clean
|
28
|
-
descriptions = @activities.sort.map(&:serialize)
|
29
|
-
names = @friends.sort.map(&:serialize)
|
30
|
-
|
31
|
-
# Write out the cleaned file.
|
32
30
|
File.open(@filename, "w") do |file|
|
33
31
|
file.puts(ACTIVITIES_HEADER)
|
34
|
-
|
35
|
-
file.puts # Blank line separating
|
32
|
+
@activities.sort.each { |act| file.puts(act.serialize) }
|
33
|
+
file.puts # Blank line separating activities from friends.
|
36
34
|
file.puts(FRIENDS_HEADER)
|
37
|
-
|
35
|
+
@friends.sort.each { |friend| file.puts(friend.serialize) }
|
36
|
+
file.puts # Blank line separating friends from locations.
|
37
|
+
file.puts(LOCATIONS_HEADER)
|
38
|
+
@locations.sort.each { |location| file.puts(location.serialize) }
|
38
39
|
end
|
39
40
|
|
40
41
|
@filename
|
41
42
|
end
|
42
43
|
|
43
|
-
# Add a friend
|
44
|
+
# Add a friend.
|
44
45
|
# @param name [String] the name of the friend to add
|
45
46
|
# @raise [FriendsError] when a friend with that name is already in the file
|
46
47
|
# @return [Friend] the added friend
|
@@ -60,7 +61,7 @@ module Friends
|
|
60
61
|
friend # Return the added friend.
|
61
62
|
end
|
62
63
|
|
63
|
-
# Add an activity
|
64
|
+
# Add an activity.
|
64
65
|
# @param serialization [String] the serialized activity
|
65
66
|
# @return [Activity] the added activity
|
66
67
|
def add_activity(serialization:)
|
@@ -70,30 +71,91 @@ module Friends
|
|
70
71
|
raise FriendsError, e
|
71
72
|
end
|
72
73
|
|
73
|
-
activity.
|
74
|
+
activity.highlight_description(introvert: self) if activity.description
|
74
75
|
@activities.unshift(activity)
|
75
76
|
|
76
77
|
activity # Return the added activity.
|
77
78
|
end
|
78
79
|
|
79
|
-
#
|
80
|
+
# Add a location.
|
81
|
+
# @param name [String] the serialized location
|
82
|
+
# @return [Location] the added location
|
83
|
+
# @raise [FriendsError] if a location with that name already exists
|
84
|
+
def add_location(name:)
|
85
|
+
if @locations.any? { |location| location.name == name }
|
86
|
+
raise FriendsError, "Location \"#{name}\" already exists"
|
87
|
+
end
|
88
|
+
|
89
|
+
begin
|
90
|
+
location = Location.deserialize(name)
|
91
|
+
rescue Serializable::SerializationError => e
|
92
|
+
raise FriendsError, e
|
93
|
+
end
|
94
|
+
|
95
|
+
@locations << location
|
96
|
+
|
97
|
+
location # Return the added location.
|
98
|
+
end
|
99
|
+
|
100
|
+
# Set a friend's location.
|
101
|
+
# @param name [String] the friend's name
|
102
|
+
# @param location_name [String] the name of an existing location
|
103
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given name
|
104
|
+
# @raise [FriendsError] if 0 or 2+ locations match the given location name
|
105
|
+
# @return [Friend] the modified friend
|
106
|
+
def set_location(name:, location_name:)
|
107
|
+
friend = friend_with_name_in(name)
|
108
|
+
location = location_with_name_in(location_name)
|
109
|
+
friend.location_name = location.name
|
110
|
+
friend
|
111
|
+
end
|
112
|
+
|
113
|
+
# Rename an existing friend.
|
80
114
|
# @param old_name [String] the name of the friend
|
81
115
|
# @param new_name [String] the new name of the friend
|
82
|
-
# @raise [FriendsError] if 0
|
116
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given name
|
83
117
|
# @return [Friend] the existing friend
|
84
118
|
def rename_friend(old_name:, new_name:)
|
85
|
-
|
119
|
+
old_name.strip!
|
120
|
+
new_name.strip!
|
121
|
+
|
122
|
+
friend = friend_with_name_in(old_name)
|
86
123
|
@activities.each do |activity|
|
87
|
-
activity.
|
124
|
+
activity.update_friend_name(old_name: friend.name, new_name: new_name)
|
88
125
|
end
|
89
|
-
friend.
|
126
|
+
friend.name = new_name
|
90
127
|
friend
|
91
128
|
end
|
92
129
|
|
93
|
-
#
|
130
|
+
# Rename an existing location.
|
131
|
+
# @param old_name [String] the name of the location
|
132
|
+
# @param new_name [String] the new name of the location
|
133
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given name
|
134
|
+
# @return [Location] the existing location
|
135
|
+
def rename_location(old_name:, new_name:)
|
136
|
+
old_name.strip!
|
137
|
+
new_name.strip!
|
138
|
+
|
139
|
+
loc = location_with_name_in(old_name)
|
140
|
+
|
141
|
+
# Update locations in activities.
|
142
|
+
@activities.each do |activity|
|
143
|
+
activity.update_location_name(old_name: loc.name, new_name: new_name)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Update locations of friends.
|
147
|
+
@friends.select { |f| f.location_name == loc.name }.each do |friend|
|
148
|
+
friend.location_name = new_name
|
149
|
+
end
|
150
|
+
|
151
|
+
loc.name = new_name # Update location itself.
|
152
|
+
loc
|
153
|
+
end
|
154
|
+
|
155
|
+
# Add a nickname to an existing friend.
|
94
156
|
# @param name [String] the name of the friend
|
95
157
|
# @param nickname [String] the nickname to add to the friend
|
96
|
-
# @raise [FriendsError] if 0
|
158
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given name
|
97
159
|
# @return [Friend] the existing friend
|
98
160
|
def add_nickname(name:, nickname:)
|
99
161
|
friend = friend_with_name_in(name)
|
@@ -105,7 +167,7 @@ module Friends
|
|
105
167
|
# file.
|
106
168
|
# @param name [String] the name of the friend
|
107
169
|
# @param nickname [String] the nickname to remove from the friend
|
108
|
-
# @raise [FriendsError] if 0
|
170
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given name
|
109
171
|
# @return [Friend] the existing friend
|
110
172
|
def remove_nickname(name:, nickname:)
|
111
173
|
friend = friend_with_name_in(name)
|
@@ -114,9 +176,19 @@ module Friends
|
|
114
176
|
end
|
115
177
|
|
116
178
|
# List all friend names in the friends file.
|
179
|
+
# @param location_name [String] the name of a location to filter by, or nil
|
180
|
+
# for unfiltered
|
117
181
|
# @return [Array] a list of all friend names
|
118
|
-
def list_friends
|
119
|
-
@friends
|
182
|
+
def list_friends(location_name:)
|
183
|
+
fs = @friends
|
184
|
+
|
185
|
+
# Filter by location if a name is passed.
|
186
|
+
if location_name
|
187
|
+
location = location_with_name_in(location_name)
|
188
|
+
fs = fs.select { |friend| friend.location_name == location.name }
|
189
|
+
end
|
190
|
+
|
191
|
+
fs.map(&:name)
|
120
192
|
end
|
121
193
|
|
122
194
|
# List your favorite friends.
|
@@ -150,9 +222,11 @@ module Friends
|
|
150
222
|
# limit
|
151
223
|
# @param with [String] the name of a friend to filter by, or nil for
|
152
224
|
# unfiltered
|
225
|
+
# @param location_name [String] the name of a location to filter by, or nil
|
226
|
+
# for unfiltered
|
153
227
|
# @return [Array] a list of all activity text values
|
154
|
-
# @raise [FriendsError] if 0
|
155
|
-
def list_activities(limit:, with:)
|
228
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given `with` text
|
229
|
+
def list_activities(limit:, with:, location_name:)
|
156
230
|
acts = @activities
|
157
231
|
|
158
232
|
# Filter by friend name if argument is passed.
|
@@ -161,24 +235,39 @@ module Friends
|
|
161
235
|
acts = acts.select { |act| act.includes_friend?(friend: friend) }
|
162
236
|
end
|
163
237
|
|
238
|
+
# Filter by location name if argument is passed.
|
239
|
+
unless location_name.nil?
|
240
|
+
location = location_with_name_in(location_name)
|
241
|
+
acts = acts.select { |act| act.includes_location?(location: location) }
|
242
|
+
end
|
243
|
+
|
164
244
|
# If we need to, trim the list.
|
165
245
|
acts = acts.take(limit) unless limit.nil?
|
166
246
|
|
167
247
|
acts.map(&:display_text)
|
168
248
|
end
|
169
249
|
|
170
|
-
#
|
171
|
-
# @
|
172
|
-
|
250
|
+
# List all location names in the friends file.
|
251
|
+
# @return [Array] a list of all location names
|
252
|
+
def list_locations
|
253
|
+
@locations.map(&:name)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Find data points for graphing activities over time.
|
257
|
+
# Optionally filter by a friend to see a given relationship over time.
|
258
|
+
#
|
259
|
+
# The returned hash uses the following format:
|
173
260
|
# {
|
174
|
-
# "Jan 2015" => 3,
|
175
|
-
# "Feb 2015" => 0,
|
261
|
+
# "Jan 2015" => 3, # The number of activities during each month.
|
262
|
+
# "Feb 2015" => 0,
|
176
263
|
# "Mar 2015" => 9
|
177
264
|
# }
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
# @
|
265
|
+
# The keys of the hash are all of the months (inclusive) between the first
|
266
|
+
# and last month in which activities have been recorded.
|
267
|
+
#
|
268
|
+
# @param name [String] the name of the friend to use
|
269
|
+
# @return [Hash{String => Fixnum}]
|
270
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given name
|
182
271
|
def graph(name: nil)
|
183
272
|
if name
|
184
273
|
friend = friend_with_name_in(name) # Find the friend by name.
|
@@ -204,17 +293,27 @@ module Friends
|
|
204
293
|
act_table
|
205
294
|
end
|
206
295
|
|
207
|
-
#
|
296
|
+
# Suggest friends to do something with.
|
297
|
+
#
|
298
|
+
# The returned hash uses the following format:
|
208
299
|
# {
|
209
300
|
# distant: ["Distant Friend 1 Name", "Distant Friend 2 Name", ...],
|
210
301
|
# moderate: ["Moderate Friend 1 Name", "Moderate Friend 2 Name", ...],
|
211
302
|
# close: ["Close Friend 1 Name", "Close Friend 2 Name", ...]
|
212
303
|
# }
|
213
|
-
|
304
|
+
#
|
305
|
+
# @param location_name [String] the name of a location to filter by, or nil
|
306
|
+
# for unfiltered
|
307
|
+
# @return [Hash{String => Array<String>}]
|
308
|
+
def suggest(location_name:)
|
214
309
|
set_n_activities! # Set n_activities for all friends.
|
215
310
|
|
311
|
+
# Filter our friends by location if necessary.
|
312
|
+
fs = @friends
|
313
|
+
fs = fs.select { |f| f.location_name == location_name } if location_name
|
314
|
+
|
216
315
|
# Sort our friends, with the least favorite friend first.
|
217
|
-
sorted_friends =
|
316
|
+
sorted_friends = fs.sort_by(&:n_activities)
|
218
317
|
|
219
318
|
output = Hash.new { |h, k| h[k] = [] }
|
220
319
|
|
@@ -255,9 +354,17 @@ module Friends
|
|
255
354
|
end
|
256
355
|
end
|
257
356
|
|
258
|
-
#
|
259
|
-
#
|
260
|
-
#
|
357
|
+
# Get a regex friend map.
|
358
|
+
#
|
359
|
+
# The returned hash uses the following format:
|
360
|
+
# {
|
361
|
+
# /regex/ => [list of friends matching regex]
|
362
|
+
# }
|
363
|
+
#
|
364
|
+
# This hash is sorted (because Ruby's hashes are ordered) by decreasing
|
365
|
+
# regex key length, so the key /Jacob Evelyn/ appears before /Jacob/.
|
366
|
+
#
|
367
|
+
# @return [Hash{Regexp => Array<Friends::Friend>}]
|
261
368
|
def regex_friend_map
|
262
369
|
@friends.each_with_object(Hash.new { |h, k| h[k] = [] }) do |friend, hash|
|
263
370
|
friend.regexes_for_name.each do |regex|
|
@@ -266,6 +373,23 @@ module Friends
|
|
266
373
|
end.sort_by { |k, _| -k.to_s.size }.to_h
|
267
374
|
end
|
268
375
|
|
376
|
+
# Get a regex location map.
|
377
|
+
#
|
378
|
+
# The returned hash uses the following format:
|
379
|
+
# {
|
380
|
+
# /regex/ => [list of friends matching regex]
|
381
|
+
# }
|
382
|
+
#
|
383
|
+
# This hash is sorted (because Ruby's hashes are ordered) by decreasing
|
384
|
+
# regex key length, so the key /Paris, France/ appears before /Paris/.
|
385
|
+
#
|
386
|
+
# @return [Hash{Regexp => Array<Friends::Location>}]
|
387
|
+
def regex_location_map
|
388
|
+
@locations.each_with_object({}) do |location, hash|
|
389
|
+
hash[location.regex_for_name] = location
|
390
|
+
end.sort_by { |k, _| -k.to_s.size }.to_h
|
391
|
+
end
|
392
|
+
|
269
393
|
# Sets the likelihood_score field on each friend in `possible_matches`. This
|
270
394
|
# score represents how likely it is that an activity containing the friends
|
271
395
|
# in `matches` and containing a friend from each group in `possible_matches`
|
@@ -285,9 +409,9 @@ module Friends
|
|
285
409
|
combination(2).
|
286
410
|
reject do |friend1, friend2|
|
287
411
|
(matches & [friend1, friend2]).size == 2 ||
|
288
|
-
|
289
|
-
|
290
|
-
|
412
|
+
possible_matches.any? do |group|
|
413
|
+
(group & [friend1, friend2]).size == 2
|
414
|
+
end
|
291
415
|
end
|
292
416
|
|
293
417
|
@activities.each do |activity|
|
@@ -326,44 +450,65 @@ module Friends
|
|
326
450
|
def read_file
|
327
451
|
@friends = []
|
328
452
|
@activities = []
|
453
|
+
@locations = []
|
329
454
|
|
330
455
|
return unless File.exist?(@filename)
|
331
456
|
|
332
|
-
state = :
|
333
|
-
line_num = 0
|
457
|
+
state = :unknown
|
334
458
|
|
335
459
|
# Loop through all lines in the file and process them.
|
336
|
-
File.foreach(@filename) do |line|
|
337
|
-
line_num += 1
|
460
|
+
File.foreach(@filename).with_index(1) do |line, line_num|
|
338
461
|
line.chomp! # Remove trailing newline from each line.
|
339
462
|
|
340
|
-
|
341
|
-
|
342
|
-
|
463
|
+
# Parse the line and update the parsing state.
|
464
|
+
state = parse_line!(line, line_num: line_num, state: state)
|
465
|
+
end
|
466
|
+
end
|
343
467
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
bad_line(e, line_num)
|
361
|
-
end
|
468
|
+
# Parse the given line, adding to the various internal data structures as
|
469
|
+
# necessary.
|
470
|
+
# @param line [String]
|
471
|
+
# @param line_num [Integer] the 1-indexed file line number we're parsing
|
472
|
+
# @param state [Symbol] the state of the parsing, one of:
|
473
|
+
# [:unknown, :reading_activities, :reading_friends, :reading_locations]
|
474
|
+
# @return [Symbol] the updated state after parsing the given line
|
475
|
+
def parse_line!(line, line_num:, state:)
|
476
|
+
return :unknown if line == ""
|
477
|
+
|
478
|
+
# If we're in an unknown state, look for a header to tell us what we're
|
479
|
+
# parsing next.
|
480
|
+
if state == :unknown
|
481
|
+
PARSING_STAGES.each do |stage|
|
482
|
+
if line == self.class.const_get("#{stage.id.to_s.upcase}_HEADER")
|
483
|
+
return "reading_#{stage.id}".to_sym
|
362
484
|
end
|
363
485
|
end
|
486
|
+
|
487
|
+
# If we made it here, we couldn't recognize a header.
|
488
|
+
bad_line("Couldn't parse line.", line_num)
|
364
489
|
end
|
490
|
+
|
491
|
+
# If we made it this far, we're parsing objects in a class.
|
492
|
+
stage = PARSING_STAGES.find { |s| state == "reading_#{s.id}".to_sym }
|
493
|
+
|
494
|
+
begin
|
495
|
+
instance_variable_get("@#{stage.id}") << stage.klass.deserialize(line)
|
496
|
+
rescue FriendsError => e
|
497
|
+
bad_line(e, line_num)
|
498
|
+
end
|
499
|
+
|
500
|
+
state
|
365
501
|
end
|
366
502
|
|
503
|
+
# Used internally by the parse_line! method above to associate stages with
|
504
|
+
# the class to create.
|
505
|
+
ParsingStage = Struct.new(:id, :klass)
|
506
|
+
PARSING_STAGES = [
|
507
|
+
ParsingStage.new(:activities, Activity),
|
508
|
+
ParsingStage.new(:friends, Friend),
|
509
|
+
ParsingStage.new(:locations, Location)
|
510
|
+
].freeze
|
511
|
+
|
367
512
|
# @param name [String] the name of the friend to search for
|
368
513
|
# @return [Friend] the friend whose name exactly matches the argument
|
369
514
|
# @raise [FriendsError] if more than one friend has the given name
|
@@ -379,9 +524,10 @@ module Friends
|
|
379
524
|
|
380
525
|
# @param text [String] the name (or substring) of the friend to search for
|
381
526
|
# @return [Friend] the friend that matches
|
382
|
-
# @raise [FriendsError] if 0
|
527
|
+
# @raise [FriendsError] if 0 or 2+ friends match the given text
|
383
528
|
def friend_with_name_in(text)
|
384
|
-
|
529
|
+
regex = Regexp.new(text, Regexp::IGNORECASE)
|
530
|
+
friends = @friends.select { |friend| friend.name.match(regex) }
|
385
531
|
|
386
532
|
case friends.size
|
387
533
|
when 1
|
@@ -395,11 +541,23 @@ module Friends
|
|
395
541
|
end
|
396
542
|
end
|
397
543
|
|
398
|
-
# @param text [String] the name (or substring) of the
|
399
|
-
# @return [
|
400
|
-
|
544
|
+
# @param text [String] the name (or substring) of the location to search for
|
545
|
+
# @return [Location] the location that matches
|
546
|
+
# @raise [FriendsError] if 0 or 2+ location match the given text
|
547
|
+
def location_with_name_in(text)
|
401
548
|
regex = Regexp.new(text, Regexp::IGNORECASE)
|
402
|
-
@
|
549
|
+
locations = @locations.select { |location| location.name.match(regex) }
|
550
|
+
|
551
|
+
case locations.size
|
552
|
+
when 1
|
553
|
+
# If exactly one location matches, use that location.
|
554
|
+
return locations.first
|
555
|
+
when 0 then raise FriendsError, "No location found for \"#{text}\""
|
556
|
+
else
|
557
|
+
raise FriendsError,
|
558
|
+
"More than one location found for \"#{text}\": "\
|
559
|
+
"#{locations.map(&:name).join(', ')}"
|
560
|
+
end
|
403
561
|
end
|
404
562
|
|
405
563
|
# Raise an error that a line in the friends file is malformed.
|