friends 0.35 → 0.36

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.
@@ -8,13 +8,6 @@ command :suggest do |suggest|
8
8
  type: Stripped
9
9
 
10
10
  suggest.action do |_, options|
11
- suggestions = @introvert.suggest(location_name: options[:in])
12
-
13
- puts "Distant friend: "\
14
- "#{Paint[suggestions[:distant].sample || 'None found', :bold, :magenta]}"
15
- puts "Moderate friend: "\
16
- "#{Paint[suggestions[:moderate].sample || 'None found', :bold, :magenta]}"
17
- puts "Close friend: "\
18
- "#{Paint[suggestions[:close].sample || 'None found', :bold, :magenta]}"
11
+ @introvert.suggest(location_name: options[:in])
19
12
  end
20
13
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  desc "Updates the `friends` program"
4
4
  command :update do |update|
5
- update.action do
5
+ update.action do |global_options|
6
6
  # rubocop:disable Lint/AssignmentInCondition
7
7
  if match = `gem search friends`.match(/^friends\s\(([^\)]+)\)$/)
8
8
  # rubocop:enable Lint/AssignmentInCondition
@@ -10,19 +10,18 @@ command :update do |update|
10
10
  if Semverse::Version.coerce(remote_version) >
11
11
  Semverse::Version.coerce(Friends::VERSION)
12
12
  `gem update friends && gem cleanup friends`
13
- @message = if $?.success?
14
- Paint[
15
- "Updated to friends #{remote_version}", :bold, :green
16
- ]
17
- else
18
- Paint[
19
- "Error updating to friends version #{remote_version}", :bold, :red
20
- ]
21
- end
13
+
14
+ unless global_options[:quiet]
15
+ if $?.success?
16
+ puts Paint["Updated to friends #{remote_version}", :bold, :green]
17
+ else
18
+ puts Paint["Error updating to friends version #{remote_version}", :bold, :red]
19
+ end
20
+ end
22
21
  else
23
- @message = Paint[
24
- "Already up-to-date (#{Friends::VERSION})", :bold, :green
25
- ]
22
+ unless global_options[:quiet]
23
+ puts Paint["Already up-to-date (#{Friends::VERSION})", :bold, :green]
24
+ end
26
25
  end
27
26
  end
28
27
  end
@@ -156,13 +156,13 @@ module Friends
156
156
  # Find the names of all friends in this description.
157
157
  # @return [Array] list of all friend names in the description
158
158
  def friend_names
159
- @_friend_names ||= @description.scan(/(?<=\*\*)\w[^\*]*(?=\*\*)/).uniq
159
+ @description.scan(/(?<=\*\*)\w[^\*]*(?=\*\*)/).uniq
160
160
  end
161
161
 
162
162
  # Find the names of all locations in this description.
163
163
  # @return [Array] list of all location names in the description
164
164
  def location_names
165
- @_location_names ||= @description.scan(/(?<=_)\w[^_]*(?=_)/).uniq
165
+ @description.scan(/(?<=_)\w[^_]*(?=_)/).uniq
166
166
  end
167
167
 
168
168
  private
@@ -15,15 +15,16 @@ require "friends/friends_error"
15
15
 
16
16
  module Friends
17
17
  class Introvert
18
- DEFAULT_FILENAME = "./friends.md".freeze
19
18
  ACTIVITIES_HEADER = "### Activities:".freeze
20
19
  NOTES_HEADER = "### Notes:".freeze
21
20
  FRIENDS_HEADER = "### Friends:".freeze
22
21
  LOCATIONS_HEADER = "### Locations:".freeze
23
22
 
24
23
  # @param filename [String] the name of the friends Markdown file
25
- def initialize(filename: DEFAULT_FILENAME)
24
+ # @param quiet [Boolean] true iff we should suppress all STDOUT output
25
+ def initialize(filename:, quiet:)
26
26
  @filename = filename
27
+ @quiet = quiet
27
28
 
28
29
  # Read in the input file. It's easier to do this now and optimize later
29
30
  # than try to overly be clever about what we read and write.
@@ -31,7 +32,25 @@ module Friends
31
32
  end
32
33
 
33
34
  # Write out the friends file with cleaned/sorted data.
34
- def clean
35
+ # @param clean_command [Boolean] true iff the command the user
36
+ # executed is `friends clean`; false if this is called as the
37
+ # result of another command
38
+ def clean(clean_command:)
39
+ friend_names = Set.new(@friends.map(&:name))
40
+ location_names = Set.new(@locations.map(&:name))
41
+
42
+ # Iterate through all events and add missing friends and
43
+ # locations.
44
+ (@activities + @notes).each do |event|
45
+ event.friend_names.each do |name|
46
+ add_friend(name: name) unless friend_names.include? name
47
+ end
48
+
49
+ event.location_names.each do |name|
50
+ add_location(name: name) unless location_names.include? name
51
+ end
52
+ end
53
+
35
54
  File.open(@filename, "w") do |file|
36
55
  file.puts(ACTIVITIES_HEADER)
37
56
  stable_sort(@activities).each { |act| file.puts(act.serialize) }
@@ -46,13 +65,14 @@ module Friends
46
65
  @locations.sort.each { |location| file.puts(location.serialize) }
47
66
  end
48
67
 
49
- @filename
68
+ # This is a special-case piece of code that lets us print a message that
69
+ # includes the filename when `friends clean` is called.
70
+ safe_puts "File cleaned: \"#{@filename}\"" if clean_command
50
71
  end
51
72
 
52
73
  # Add a friend.
53
74
  # @param name [String] the name of the friend to add
54
75
  # @raise [FriendsError] when a friend with that name is already in the file
55
- # @return [Friend] the added friend
56
76
  def add_friend(name:)
57
77
  if @friends.any? { |friend| friend.name == name }
58
78
  raise FriendsError, "Friend named \"#{name}\" already exists"
@@ -62,32 +82,45 @@ module Friends
62
82
 
63
83
  @friends << friend
64
84
 
65
- friend # Return the added friend.
85
+ safe_puts "Friend added: \"#{friend.name}\""
66
86
  end
67
87
 
68
88
  # Add an activity.
69
89
  # @param serialization [String] the serialized activity
70
- # @return [Activity] the added activity
71
90
  def add_activity(serialization:)
72
91
  Activity.deserialize(serialization).tap do |activity|
73
- activity.highlight_description(introvert: self) if activity.description
92
+ # If there's no description, prompt the user for one.
93
+ if activity.description.nil? || activity.description.empty?
94
+ activity.description = Readline.readline(activity.to_s)
95
+ end
96
+
97
+ activity.highlight_description(introvert: self)
98
+
74
99
  @activities.unshift(activity)
100
+
101
+ safe_puts "Activity added: \"#{activity}\""
75
102
  end
76
103
  end
77
104
 
78
105
  # Add a note.
79
106
  # @param serialization [String] the serialized note
80
- # @return [Note] the added note
81
107
  def add_note(serialization:)
82
108
  Note.deserialize(serialization).tap do |note|
83
- note.highlight_description(introvert: self) if note.description
109
+ # If there's no description, prompt the user for one.
110
+ if note.description.nil? || note.description.empty?
111
+ note.description = Readline.readline(note.to_s)
112
+ end
113
+
114
+ note.highlight_description(introvert: self)
115
+
84
116
  @notes.unshift(note)
117
+
118
+ safe_puts "Note added: \"#{note}\""
85
119
  end
86
120
  end
87
121
 
88
122
  # Add a location.
89
123
  # @param name [String] the serialized location
90
- # @return [Location] the added location
91
124
  # @raise [FriendsError] if a location with that name already exists
92
125
  def add_location(name:)
93
126
  if @locations.any? { |location| location.name == name }
@@ -98,7 +131,7 @@ module Friends
98
131
 
99
132
  @locations << location
100
133
 
101
- location # Return the added location.
134
+ safe_puts "Location added: \"#{location.name}\"" # Return the added location.
102
135
  end
103
136
 
104
137
  # Set a friend's location.
@@ -106,39 +139,38 @@ module Friends
106
139
  # @param location_name [String] the name of an existing location
107
140
  # @raise [FriendsError] if 0 or 2+ friends match the given name
108
141
  # @raise [FriendsError] if 0 or 2+ locations match the given location name
109
- # @return [Friend] the modified friend
110
142
  def set_location(name:, location_name:)
111
143
  friend = thing_with_name_in(:friend, name)
112
144
  location = thing_with_name_in(:location, location_name)
113
145
  friend.location_name = location.name
114
- friend
146
+
147
+ safe_puts "#{friend.name}'s location set to: \"#{location.name}\""
115
148
  end
116
149
 
117
150
  # Rename an existing friend.
118
151
  # @param old_name [String] the name of the friend
119
152
  # @param new_name [String] the new name of the friend
120
153
  # @raise [FriendsError] if 0 or 2+ friends match the given name
121
- # @return [Friend] the existing friend
122
154
  def rename_friend(old_name:, new_name:)
123
155
  friend = thing_with_name_in(:friend, old_name)
124
- @activities.each do |activity|
125
- activity.update_friend_name(old_name: friend.name, new_name: new_name)
156
+ (@activities + @notes).each do |event|
157
+ event.update_friend_name(old_name: friend.name, new_name: new_name)
126
158
  end
127
159
  friend.name = new_name
128
- friend
160
+
161
+ safe_puts "Name changed: \"#{friend}\""
129
162
  end
130
163
 
131
164
  # Rename an existing location.
132
165
  # @param old_name [String] the name of the location
133
166
  # @param new_name [String] the new name of the location
134
167
  # @raise [FriendsError] if 0 or 2+ friends match the given name
135
- # @return [Location] the existing location
136
168
  def rename_location(old_name:, new_name:)
137
169
  loc = thing_with_name_in(:location, old_name)
138
170
 
139
- # Update locations in activities.
140
- @activities.each do |activity|
141
- activity.update_location_name(old_name: loc.name, new_name: new_name)
171
+ # Update locations in activities and notes.
172
+ (@activities + @notes).each do |event|
173
+ event.update_location_name(old_name: loc.name, new_name: new_name)
142
174
  end
143
175
 
144
176
  # Update locations of friends.
@@ -147,29 +179,30 @@ module Friends
147
179
  end
148
180
 
149
181
  loc.name = new_name # Update location itself.
150
- loc
182
+
183
+ safe_puts "Location renamed: \"#{loc.name}\""
151
184
  end
152
185
 
153
186
  # Add a nickname to an existing friend.
154
187
  # @param name [String] the name of the friend
155
188
  # @param nickname [String] the nickname to add to the friend
156
189
  # @raise [FriendsError] if 0 or 2+ friends match the given name
157
- # @return [Friend] the existing friend
158
190
  def add_nickname(name:, nickname:)
159
191
  friend = thing_with_name_in(:friend, name)
160
192
  friend.add_nickname(nickname)
161
- friend
193
+
194
+ safe_puts "Nickname added: \"#{friend}\""
162
195
  end
163
196
 
164
197
  # Add a tag to an existing friend.
165
198
  # @param name [String] the name of the friend
166
199
  # @param tag [String] the tag to add to the friend, of the form: "@tag"
167
200
  # @raise [FriendsError] if 0 or 2+ friends match the given name
168
- # @return [Friend] the existing friend
169
201
  def add_tag(name:, tag:)
170
202
  friend = thing_with_name_in(:friend, name)
171
203
  friend.add_tag(tag)
172
- friend
204
+
205
+ safe_puts "Tag added to friend: \"#{friend}\""
173
206
  end
174
207
 
175
208
  # Remove a tag from an existing friend.
@@ -177,11 +210,11 @@ module Friends
177
210
  # @param tag [String] the tag to remove from the friend, of the form: "@tag"
178
211
  # @raise [FriendsError] if 0 or 2+ friends match the given name
179
212
  # @raise [FriendsError] if the friend does not have the given nickname
180
- # @return [Friend] the existing friend
181
213
  def remove_tag(name:, tag:)
182
214
  friend = thing_with_name_in(:friend, name)
183
215
  friend.remove_tag(tag)
184
- friend
216
+
217
+ safe_puts "Tag removed from friend: \"#{friend}\""
185
218
  end
186
219
 
187
220
  # Remove a nickname from an existing friend.
@@ -189,11 +222,11 @@ module Friends
189
222
  # @param nickname [String] the nickname to remove from the friend
190
223
  # @raise [FriendsError] if 0 or 2+ friends match the given name
191
224
  # @raise [FriendsError] if the friend does not have the given nickname
192
- # @return [Friend] the existing friend
193
225
  def remove_nickname(name:, nickname:)
194
226
  friend = thing_with_name_in(:friend, name)
195
227
  friend.remove_nickname(nickname)
196
- friend
228
+
229
+ safe_puts "Nickname removed: \"#{friend}\""
197
230
  end
198
231
 
199
232
  # List all friend names in the friends file.
@@ -203,7 +236,6 @@ module Friends
203
236
  # unfiltered
204
237
  # @param verbose [Boolean] true iff we should output friend names with
205
238
  # nicknames, locations, and tags; false for names only
206
- # @return [Array] a list of all friend names
207
239
  def list_friends(location_name:, tagged:, verbose:)
208
240
  fs = @friends
209
241
 
@@ -220,69 +252,41 @@ module Friends
220
252
  end
221
253
  end
222
254
 
223
- verbose ? fs.map(&:to_s) : fs.map(&:name)
255
+ safe_puts(verbose ? fs.map(&:to_s) : fs.map(&:name))
224
256
  end
225
257
 
226
258
  # List your favorite friends.
227
259
  # @param limit [Integer] the number of favorite friends to return
228
- # @return [Array] a list of the favorite friends' names and activity
229
- # counts
230
260
  def list_favorite_friends(limit:)
231
261
  list_favorite_things(:friend, limit: limit)
232
262
  end
233
263
 
234
264
  # List your favorite friends.
235
265
  # @param limit [Integer] the number of favorite locations to return
236
- # @return [Array] a list of the favorite locations' names and activity
237
- # counts
238
266
  def list_favorite_locations(limit:)
239
267
  list_favorite_things(:location, limit: limit)
240
268
  end
241
269
 
242
270
  # See `list_events` for all of the parameters we can pass.
243
- # @return [Array] a list of all activities' text values
244
271
  def list_activities(**args)
245
272
  list_events(events: @activities, **args)
246
273
  end
247
274
 
248
275
  # See `list_events` for all of the parameters we can pass.
249
- # @return [Array] a list of all notes' text values
250
276
  def list_notes(**args)
251
277
  list_events(events: @notes, **args)
252
278
  end
253
279
 
254
280
  # List all location names in the friends file.
255
- # @return [Array] a list of all location names
256
281
  def list_locations
257
- @locations.map(&:name)
282
+ safe_puts @locations.map(&:name)
258
283
  end
259
284
 
260
285
  # @param from [Array] containing any of: ["activities", "friends", "notes"]
261
286
  # If not empty, limits the tags returned to only those from either
262
287
  # activities, notes, or friends.
263
- # @return [Array] a sorted list of all tags in activity descriptions
264
288
  def list_tags(from:)
265
- output = Set.new
266
-
267
- if from.empty? || from.include?("activities")
268
- @activities.each_with_object(output) do |activity, set|
269
- set.merge(activity.tags)
270
- end
271
- end
272
-
273
- if from.empty? || from.include?("notes")
274
- @notes.each_with_object(output) do |note, set|
275
- set.merge(note.tags)
276
- end
277
- end
278
-
279
- if from.empty? || from.include?("friends")
280
- @friends.each_with_object(output) do |friend, set|
281
- set.merge(friend.tags)
282
- end
283
- end
284
-
285
- output.sort_by(&:downcase)
289
+ safe_puts tags(from: from).sort_by(&:downcase)
286
290
  end
287
291
 
288
292
  # Find data points for graphing activities over time.
@@ -305,7 +309,6 @@ module Friends
305
309
  # unfiltered
306
310
  # @param since_date [Date] a date on or after which to find activities, or nil for unfiltered
307
311
  # @param until_date [Date] a date before or on which to find activities, or nil for unfiltered
308
- # @return [Hash{String => Integer}]
309
312
  # @raise [FriendsError] if friend, location or tag cannot be found or
310
313
  # is ambiguous
311
314
  def graph(with:, location_name:, tagged:, since_date:, until_date:)
@@ -350,7 +353,6 @@ module Friends
350
353
  #
351
354
  # @param location_name [String] the name of a location to filter by, or nil
352
355
  # for unfiltered
353
- # @return [Hash{String => Array<String>}]
354
356
  def suggest(location_name:)
355
357
  # Filter our friends by location if necessary.
356
358
  fs = @friends
@@ -359,22 +361,25 @@ module Friends
359
361
  # Sort our friends, with the least favorite friend first.
360
362
  sorted_friends = fs.sort_by(&:n_activities)
361
363
 
362
- output = Hash.new { |h, k| h[k] = [] }
363
-
364
364
  # Set initial value in case there are no friends and the while loop is
365
365
  # never entered.
366
- output[:distant] = []
366
+ distant_friend_names = []
367
367
 
368
368
  # First, get not-so-good friends.
369
369
  while !sorted_friends.empty? && sorted_friends.first.n_activities < 2
370
- output[:distant] << sorted_friends.shift.name
370
+ distant_friend_names << sorted_friends.shift.name
371
371
  end
372
372
 
373
- output[:moderate] = sorted_friends.slice!(0, sorted_friends.size * 3 / 4).
374
- map!(&:name)
375
- output[:close] = sorted_friends.map!(&:name)
373
+ moderate_friend_names = sorted_friends.slice!(0, sorted_friends.size * 3 / 4).
374
+ map!(&:name)
375
+ close_friend_names = sorted_friends.map!(&:name)
376
376
 
377
- output
377
+ safe_puts "Distant friend: "\
378
+ "#{Paint[distant_friend_names.sample || 'None found', :bold, :magenta]}"
379
+ safe_puts "Moderate friend: "\
380
+ "#{Paint[moderate_friend_names.sample || 'None found', :bold, :magenta]}"
381
+ safe_puts "Close friend: "\
382
+ "#{Paint[close_friend_names.sample || 'None found', :bold, :magenta]}"
378
383
  end
379
384
 
380
385
  ###################################################################
@@ -452,41 +457,50 @@ module Friends
452
457
  end
453
458
  end
454
459
 
455
- # @return [Integer] the total number of friends
456
- def total_friends
457
- @friends.size
458
- end
460
+ def stats
461
+ safe_puts "Total activities: #{@activities.size}"
462
+ safe_puts "Total friends: #{@friends.size}"
463
+ safe_puts "Total locations: #{@locations.size}"
464
+ safe_puts "Total notes: #{@notes.size}"
465
+ safe_puts "Total tags: #{tags.size}"
459
466
 
460
- # @return [Integer] the total number of activities
461
- def total_activities
462
- @activities.size
463
- end
467
+ events = @activities + @notes
464
468
 
465
- # @return [Integer] the total number of locations
466
- def total_locations
467
- @locations.size
468
- end
469
+ elapsed_days = if events.size < 2
470
+ 0
471
+ else
472
+ sorted_events = events.sort
473
+ (sorted_events.first.date - sorted_events.last.date).to_i
474
+ end
469
475
 
470
- # @return [Integer] the total number of notes
471
- def total_notes
472
- @notes.size
476
+ safe_puts "Total time elapsed: #{elapsed_days} day#{'s' if elapsed_days != 1}"
473
477
  end
474
478
 
475
- # @return [Integer] the total number of tags
476
- def total_tags
477
- list_tags(from: []).size
478
- end
479
+ private
479
480
 
480
- # @return [Integer] the number of days elapsed between
481
- # the first and last activity
482
- def elapsed_days
483
- events = @activities + @notes
484
- return 0 if events.size < 2
485
- sorted_events = events.sort
486
- (sorted_events.first.date - sorted_events.last.date).to_i
481
+ # Print the message unless we're in `quiet` mode.
482
+ # @param str [String] a message to print
483
+ def safe_puts(str)
484
+ puts str unless @quiet
487
485
  end
488
486
 
489
- private
487
+ # @param from [Array] containing any of: ["activities", "friends", "notes"]
488
+ # If not empty, limits the tags returned to only those from either
489
+ # activities, notes, or friends.
490
+ # @return [Set] the set of tags present in the given things
491
+ def tags(from: [])
492
+ Set.new.tap do |output|
493
+ if from.empty? || from.include?("activities")
494
+ @activities.each { |activity| output.merge(activity.tags) }
495
+ end
496
+
497
+ @notes.each { |note| output.merge(note.tags) } if from.empty? || from.include?("notes")
498
+
499
+ if from.empty? || from.include?("friends")
500
+ @friends.each { |friend| output.merge(friend.tags) }
501
+ end
502
+ end
503
+ end
490
504
 
491
505
  # List all event details.
492
506
  # @param events [Array<Event>] the base events to list, either @activities or @notes
@@ -500,7 +514,6 @@ module Friends
500
514
  # unfiltered
501
515
  # @param since_date [Date] a date on or after which to find events, or nil for unfiltered
502
516
  # @param until_date [Date] a date before or on which to find events, or nil for unfiltered
503
- # @return [Array] a list of all event (activity or note) text values
504
517
  # @raise [ArgumentError] if limit is present but limit < 1
505
518
  # @raise [FriendsError] if friend, location or tag cannot be found or
506
519
  # is ambiguous
@@ -519,7 +532,7 @@ module Friends
519
532
  # If we need to, trim the list.
520
533
  events = events.take(limit) unless limit.nil?
521
534
 
522
- events.map(&:to_s)
535
+ safe_puts events.map(&:to_s)
523
536
  end
524
537
 
525
538
  # @param arr [Array] an unsorted array
@@ -538,7 +551,7 @@ module Friends
538
551
  # unfiltered
539
552
  # @param since_date [Date] a date on or after which to find activities, or nil for unfiltered
540
553
  # @param until_date [Date] a date before or on which to find activities, or nil for unfiltered
541
- # @return [Array] an array of activities
554
+ # @return [Array] an array of activities or notes
542
555
  # @raise [FriendsError] if friend, location or tag cannot be found or
543
556
  # is ambiguous
544
557
  def filtered_events(events:, with:, location_name:, tagged:, since_date:, until_date:)
@@ -572,7 +585,6 @@ module Friends
572
585
 
573
586
  # @param type [Symbol] one of: [:friend, :location]
574
587
  # @param limit [Integer] the number of favorite things to return
575
- # @return [Array] a list of the favorite things' names and activity counts
576
588
  # @raise [ArgumentError] if type is not one of: [:friend, :location]
577
589
  # @raise [ArgumentError] if limit is < 1
578
590
  def list_favorite_things(type, limit:)
@@ -589,12 +601,12 @@ module Friends
589
601
 
590
602
  if results.size == 1
591
603
  favorite = results.first
592
- puts "Your favorite #{type} is "\
593
- "#{favorite.name} "\
594
- "(#{favorite.n_activities} "\
595
- "#{favorite.n_activities == 1 ? 'activity' : 'activities'})"
604
+ safe_puts "Your favorite #{type} is "\
605
+ "#{favorite.name} "\
606
+ "(#{favorite.n_activities} "\
607
+ "#{favorite.n_activities == 1 ? 'activity' : 'activities'})"
596
608
  else
597
- puts "Your favorite #{type}s:"
609
+ safe_puts "Your favorite #{type}s:"
598
610
 
599
611
  max_str_size = results.map(&:name).map(&:size).max
600
612
 
@@ -621,7 +633,7 @@ module Friends
621
633
  # elements, which may be too large if the last element in the list is a tie.
622
634
  num_str_size = data.last.first.to_s.size + 1 unless data.empty?
623
635
  data.each do |ranking, str|
624
- puts "#{"#{ranking}.".ljust(num_str_size)} #{str}"
636
+ safe_puts "#{"#{ranking}.".ljust(num_str_size)} #{str}"
625
637
  end
626
638
  end
627
639
  end