friends 0.16 → 0.17
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|