friends 0.39 → 0.40
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +17 -2
- data/README.md +93 -95
- data/bin/friends +13 -4
- data/friends.gemspec +15 -10
- data/lib/friends/commands/edit.rb +1 -4
- data/lib/friends/commands/list.rb +4 -23
- data/lib/friends/event.rb +2 -0
- data/lib/friends/graph.rb +6 -5
- data/lib/friends/introvert.rb +65 -98
- data/lib/friends/version.rb +1 -1
- data/test/commands/graph_spec.rb +94 -94
- data/test/commands/list/activities_spec.rb +0 -21
- data/test/commands/list/favorite/friends_spec.rb +0 -48
- data/test/commands/list/favorite/locations_spec.rb +0 -53
- data/test/commands/list/notes_spec.rb +0 -21
- data/test/paging_spec.rb +34 -0
- metadata +21 -4
@@ -10,10 +10,7 @@ command :edit do |edit|
|
|
10
10
|
|
11
11
|
# Mark the file for cleaning once the editor was closed correctly.
|
12
12
|
if Kernel.system("#{editor} #{filename}")
|
13
|
-
@introvert = Friends::Introvert.new(
|
14
|
-
filename: global_options[:filename],
|
15
|
-
quiet: global_options[:quiet]
|
16
|
-
)
|
13
|
+
@introvert = Friends::Introvert.new(filename: global_options[:filename])
|
17
14
|
@clean_command = true
|
18
15
|
@dirty = true
|
19
16
|
elsif !global_options[:quiet]
|
@@ -31,12 +31,6 @@ command :list do |list|
|
|
31
31
|
[:activities, :notes].each do |events|
|
32
32
|
list.desc "Lists all #{events}"
|
33
33
|
list.command events do |list_events|
|
34
|
-
list_events.flag [:limit],
|
35
|
-
arg_name: "NUMBER",
|
36
|
-
desc: "The number of #{events} to return",
|
37
|
-
default_value: 10,
|
38
|
-
type: Integer
|
39
|
-
|
40
34
|
list_events.flag [:with],
|
41
35
|
arg_name: "NAME",
|
42
36
|
desc: "List only #{events} with the given friend",
|
@@ -67,7 +61,6 @@ command :list do |list|
|
|
67
61
|
list_events.action do |_, options|
|
68
62
|
@introvert.send(
|
69
63
|
"list_#{events}",
|
70
|
-
limit: options[:limit],
|
71
64
|
with: options[:with],
|
72
65
|
location_name: options[:in],
|
73
66
|
tagged: options[:tagged],
|
@@ -101,27 +94,15 @@ command :list do |list|
|
|
101
94
|
list.command :favorite do |list_favorite|
|
102
95
|
list_favorite.desc "List favorite friends"
|
103
96
|
list_favorite.command :friends do |list_favorite_friends|
|
104
|
-
list_favorite_friends.
|
105
|
-
|
106
|
-
desc: "The number of friends to return",
|
107
|
-
default_value: 10,
|
108
|
-
type: Integer
|
109
|
-
|
110
|
-
list_favorite_friends.action do |_, options|
|
111
|
-
@introvert.list_favorite_friends(limit: options[:limit])
|
97
|
+
list_favorite_friends.action do
|
98
|
+
@introvert.list_favorite_friends
|
112
99
|
end
|
113
100
|
end
|
114
101
|
|
115
102
|
list_favorite.desc "List favorite locations"
|
116
103
|
list_favorite.command :locations do |list_favorite_locations|
|
117
|
-
list_favorite_locations.
|
118
|
-
|
119
|
-
desc: "The number of locations to return",
|
120
|
-
default_value: 10,
|
121
|
-
type: Integer
|
122
|
-
|
123
|
-
list_favorite_locations.action do |_, options|
|
124
|
-
@introvert.list_favorite_locations(limit: options[:limit])
|
104
|
+
list_favorite_locations.action do
|
105
|
+
@introvert.list_favorite_locations
|
125
106
|
end
|
126
107
|
end
|
127
108
|
end
|
data/lib/friends/event.rb
CHANGED
@@ -291,7 +291,9 @@ module Friends
|
|
291
291
|
def description_matches(regex:, replace:, indicator:)
|
292
292
|
# rubocop:disable Lint/AssignmentInCondition
|
293
293
|
return unless match = @description.match(regex) # Abort if no match.
|
294
|
+
|
294
295
|
# rubocop:enable Lint/AssignmentInCondition
|
296
|
+
|
295
297
|
str = yield # It's important to execute the block even if not replacing.
|
296
298
|
return unless replace # Only continue if we want to replace text.
|
297
299
|
|
data/lib/friends/graph.rb
CHANGED
@@ -17,9 +17,9 @@ module Friends
|
|
17
17
|
@end_date = @all_activities.first.date
|
18
18
|
end
|
19
19
|
|
20
|
-
#
|
21
|
-
def
|
22
|
-
to_h.
|
20
|
+
# @return [Array<String>] the output to print, with colors
|
21
|
+
def output
|
22
|
+
to_h.map do |month, (filtered_count, total_count)|
|
23
23
|
str = "#{month} |"
|
24
24
|
str += Array.new(filtered_count) do |count|
|
25
25
|
Paint["█", color(count)]
|
@@ -29,8 +29,9 @@ module Friends
|
|
29
29
|
Paint["∙", color(filtered_count + count)]
|
30
30
|
end.join + Paint["|", color(total_count + 1)]
|
31
31
|
end
|
32
|
-
|
33
|
-
|
32
|
+
|
33
|
+
str
|
34
|
+
end.reverse!
|
34
35
|
end
|
35
36
|
|
36
37
|
private
|
data/lib/friends/introvert.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
# the command-line script explicitly.
|
6
6
|
|
7
7
|
require "set"
|
8
|
+
require "tty/pager"
|
8
9
|
|
9
10
|
require "friends/activity"
|
10
11
|
require "friends/note"
|
@@ -21,16 +22,17 @@ module Friends
|
|
21
22
|
LOCATIONS_HEADER = "### Locations:".freeze
|
22
23
|
|
23
24
|
# @param filename [String] the name of the friends Markdown file
|
24
|
-
|
25
|
-
def initialize(filename:, quiet:)
|
25
|
+
def initialize(filename:)
|
26
26
|
@filename = filename
|
27
|
-
@
|
27
|
+
@output = []
|
28
28
|
|
29
29
|
# Read in the input file. It's easier to do this now and optimize later
|
30
30
|
# than try to overly be clever about what we read and write.
|
31
31
|
read_file
|
32
32
|
end
|
33
33
|
|
34
|
+
attr_reader :output
|
35
|
+
|
34
36
|
# Write out the friends file with cleaned/sorted data.
|
35
37
|
# @param clean_command [Boolean] true iff the command the user
|
36
38
|
# executed is `friends clean`; false if this is called as the
|
@@ -73,7 +75,7 @@ module Friends
|
|
73
75
|
|
74
76
|
# This is a special-case piece of code that lets us print a message that
|
75
77
|
# includes the filename when `friends clean` is called.
|
76
|
-
|
78
|
+
@output << "File cleaned: \"#{@filename}\"" if clean_command
|
77
79
|
end
|
78
80
|
|
79
81
|
# Add a friend.
|
@@ -88,7 +90,7 @@ module Friends
|
|
88
90
|
|
89
91
|
@friends << friend
|
90
92
|
|
91
|
-
|
93
|
+
@output << "Friend added: \"#{friend.name}\""
|
92
94
|
end
|
93
95
|
|
94
96
|
# Add an activity.
|
@@ -106,7 +108,7 @@ module Friends
|
|
106
108
|
|
107
109
|
@activities.unshift(activity)
|
108
110
|
|
109
|
-
|
111
|
+
@output << "Activity added: \"#{activity}\""
|
110
112
|
end
|
111
113
|
end
|
112
114
|
|
@@ -125,7 +127,7 @@ module Friends
|
|
125
127
|
|
126
128
|
@notes.unshift(note)
|
127
129
|
|
128
|
-
|
130
|
+
@output << "Note added: \"#{note}\""
|
129
131
|
end
|
130
132
|
end
|
131
133
|
|
@@ -141,7 +143,7 @@ module Friends
|
|
141
143
|
|
142
144
|
@locations << location
|
143
145
|
|
144
|
-
|
146
|
+
@output << "Location added: \"#{location.name}\"" # Return the added location.
|
145
147
|
end
|
146
148
|
|
147
149
|
# Set a friend's location.
|
@@ -154,7 +156,7 @@ module Friends
|
|
154
156
|
location = thing_with_name_in(:location, location_name)
|
155
157
|
friend.location_name = location.name
|
156
158
|
|
157
|
-
|
159
|
+
@output << "#{friend.name}'s location set to: \"#{location.name}\""
|
158
160
|
end
|
159
161
|
|
160
162
|
# Rename an existing friend.
|
@@ -168,7 +170,7 @@ module Friends
|
|
168
170
|
end
|
169
171
|
friend.name = new_name
|
170
172
|
|
171
|
-
|
173
|
+
@output << "Name changed: \"#{friend}\""
|
172
174
|
end
|
173
175
|
|
174
176
|
# Rename an existing location.
|
@@ -190,7 +192,7 @@ module Friends
|
|
190
192
|
|
191
193
|
loc.name = new_name # Update location itself.
|
192
194
|
|
193
|
-
|
195
|
+
@output << "Location renamed: \"#{loc.name}\""
|
194
196
|
end
|
195
197
|
|
196
198
|
# Add a nickname to an existing friend.
|
@@ -203,7 +205,7 @@ module Friends
|
|
203
205
|
friend = thing_with_name_in(:friend, name)
|
204
206
|
friend.add_nickname(nickname)
|
205
207
|
|
206
|
-
|
208
|
+
@output << "Nickname added: \"#{friend}\""
|
207
209
|
end
|
208
210
|
|
209
211
|
# Add a tag to an existing friend.
|
@@ -216,7 +218,7 @@ module Friends
|
|
216
218
|
friend = thing_with_name_in(:friend, name)
|
217
219
|
friend.add_tag(tag)
|
218
220
|
|
219
|
-
|
221
|
+
@output << "Tag added to friend: \"#{friend}\""
|
220
222
|
end
|
221
223
|
|
222
224
|
# Remove a tag from an existing friend.
|
@@ -228,7 +230,7 @@ module Friends
|
|
228
230
|
friend = thing_with_name_in(:friend, name)
|
229
231
|
friend.remove_tag(tag)
|
230
232
|
|
231
|
-
|
233
|
+
@output << "Tag removed from friend: \"#{friend}\""
|
232
234
|
end
|
233
235
|
|
234
236
|
# Remove a nickname from an existing friend.
|
@@ -240,7 +242,7 @@ module Friends
|
|
240
242
|
friend = thing_with_name_in(:friend, name)
|
241
243
|
friend.remove_nickname(nickname)
|
242
244
|
|
243
|
-
|
245
|
+
@output << "Nickname removed: \"#{friend}\""
|
244
246
|
end
|
245
247
|
|
246
248
|
# List all friend names in the friends file.
|
@@ -266,19 +268,17 @@ module Friends
|
|
266
268
|
end
|
267
269
|
end
|
268
270
|
|
269
|
-
|
271
|
+
(verbose ? fs.map(&:to_s) : fs.map(&:name)).each { |line| @output << line }
|
270
272
|
end
|
271
273
|
|
272
274
|
# List your favorite friends.
|
273
|
-
|
274
|
-
|
275
|
-
list_favorite_things(:friend, limit: limit)
|
275
|
+
def list_favorite_friends
|
276
|
+
list_favorite_things(:friend)
|
276
277
|
end
|
277
278
|
|
278
279
|
# List your favorite friends.
|
279
|
-
|
280
|
-
|
281
|
-
list_favorite_things(:location, limit: limit)
|
280
|
+
def list_favorite_locations
|
281
|
+
list_favorite_things(:location)
|
282
282
|
end
|
283
283
|
|
284
284
|
# See `list_events` for all of the parameters we can pass.
|
@@ -293,26 +293,20 @@ module Friends
|
|
293
293
|
|
294
294
|
# List all location names in the friends file.
|
295
295
|
def list_locations
|
296
|
-
|
296
|
+
@locations.each { |location| @output << location.name }
|
297
297
|
end
|
298
298
|
|
299
299
|
# @param from [Array] containing any of: ["activities", "friends", "notes"]
|
300
300
|
# If not empty, limits the tags returned to only those from either
|
301
301
|
# activities, notes, or friends.
|
302
302
|
def list_tags(from:)
|
303
|
-
|
303
|
+
tags(from: from).sort_by(&:downcase).each { |tag| @output << tag }
|
304
304
|
end
|
305
305
|
|
306
|
-
#
|
306
|
+
# Graph activities over time.
|
307
307
|
# Optionally filter by friend, location and tag
|
308
308
|
#
|
309
|
-
# The
|
310
|
-
# {
|
311
|
-
# "Jan 2015" => 3, # The number of activities during each month.
|
312
|
-
# "Feb 2015" => 0,
|
313
|
-
# "Mar 2015" => 9
|
314
|
-
# }
|
315
|
-
# The keys of the hash are all of the months (inclusive) between the first
|
309
|
+
# The graph displays all of the months (inclusive) between the first
|
316
310
|
# and last month in which activities have been recorded.
|
317
311
|
#
|
318
312
|
# @param with [Array<String>] the names of friends to filter by, or empty for
|
@@ -353,7 +347,7 @@ module Friends
|
|
353
347
|
Graph.new(
|
354
348
|
filtered_activities: filtered_activities_to_graph,
|
355
349
|
all_activities: all_activities_to_graph
|
356
|
-
).
|
350
|
+
).output.each { |line| @output << line }
|
357
351
|
end
|
358
352
|
|
359
353
|
# Suggest friends to do something with.
|
@@ -388,12 +382,12 @@ module Friends
|
|
388
382
|
map!(&:name)
|
389
383
|
close_friend_names = sorted_friends.map!(&:name)
|
390
384
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
385
|
+
@output << "Distant friend: "\
|
386
|
+
"#{Paint[distant_friend_names.sample || 'None found', :bold, :magenta]}"
|
387
|
+
@output << "Moderate friend: "\
|
388
|
+
"#{Paint[moderate_friend_names.sample || 'None found', :bold, :magenta]}"
|
389
|
+
@output << "Close friend: "\
|
390
|
+
"#{Paint[close_friend_names.sample || 'None found', :bold, :magenta]}"
|
397
391
|
end
|
398
392
|
|
399
393
|
###################################################################
|
@@ -472,12 +466,6 @@ module Friends
|
|
472
466
|
end
|
473
467
|
|
474
468
|
def stats
|
475
|
-
safe_puts "Total activities: #{@activities.size}"
|
476
|
-
safe_puts "Total friends: #{@friends.size}"
|
477
|
-
safe_puts "Total locations: #{@locations.size}"
|
478
|
-
safe_puts "Total notes: #{@notes.size}"
|
479
|
-
safe_puts "Total tags: #{tags.size}"
|
480
|
-
|
481
469
|
events = @activities + @notes
|
482
470
|
|
483
471
|
elapsed_days = if events.size < 2
|
@@ -487,17 +475,16 @@ module Friends
|
|
487
475
|
(sorted_events.first.date - sorted_events.last.date).to_i
|
488
476
|
end
|
489
477
|
|
490
|
-
|
478
|
+
@output << "Total activities: #{@activities.size}"
|
479
|
+
@output << "Total friends: #{@friends.size}"
|
480
|
+
@output << "Total locations: #{@locations.size}"
|
481
|
+
@output << "Total notes: #{@notes.size}"
|
482
|
+
@output << "Total tags: #{tags.size}"
|
483
|
+
@output << "Total time elapsed: #{elapsed_days} day#{'s' if elapsed_days != 1}"
|
491
484
|
end
|
492
485
|
|
493
486
|
private
|
494
487
|
|
495
|
-
# Print the message unless we're in `quiet` mode.
|
496
|
-
# @param str [String] a message to print
|
497
|
-
def safe_puts(str)
|
498
|
-
puts str unless @quiet
|
499
|
-
end
|
500
|
-
|
501
488
|
# @param from [Array] containing any of: ["activities", "friends", "notes"]
|
502
489
|
# If not empty, limits the tags returned to only those from either
|
503
490
|
# activities, notes, or friends.
|
@@ -518,8 +505,6 @@ module Friends
|
|
518
505
|
|
519
506
|
# List all event details.
|
520
507
|
# @param events [Array<Event>] the base events to list, either @activities or @notes
|
521
|
-
# @param limit [Integer] the number of events to return, or nil for no
|
522
|
-
# limit
|
523
508
|
# @param with [Array<String>] the names of friends to filter by, or empty for
|
524
509
|
# unfiltered
|
525
510
|
# @param location_name [String] the name of a location to filter by, or
|
@@ -528,12 +513,9 @@ module Friends
|
|
528
513
|
# unfiltered
|
529
514
|
# @param since_date [Date] a date on or after which to find events, or nil for unfiltered
|
530
515
|
# @param until_date [Date] a date before or on which to find events, or nil for unfiltered
|
531
|
-
# @raise [ArgumentError] if limit is present but limit < 1
|
532
516
|
# @raise [FriendsError] if friend, location or tag cannot be found or
|
533
517
|
# is ambiguous
|
534
|
-
def list_events(events:,
|
535
|
-
raise ArgumentError, "Limit must be positive" if limit && limit < 1
|
536
|
-
|
518
|
+
def list_events(events:, with:, location_name:, tagged:, since_date:, until_date:)
|
537
519
|
events = filtered_events(
|
538
520
|
events: events,
|
539
521
|
with: with,
|
@@ -543,10 +525,7 @@ module Friends
|
|
543
525
|
until_date: until_date
|
544
526
|
)
|
545
527
|
|
546
|
-
|
547
|
-
events = events.take(limit) unless limit.nil?
|
548
|
-
|
549
|
-
safe_puts events.map(&:to_s)
|
528
|
+
events.each { |event| @output << event.to_s }
|
550
529
|
end
|
551
530
|
|
552
531
|
# @param arr [Array] an unsorted array
|
@@ -598,57 +577,45 @@ module Friends
|
|
598
577
|
end
|
599
578
|
|
600
579
|
# @param type [Symbol] one of: [:friend, :location]
|
601
|
-
# @param limit [Integer] the number of favorite things to return
|
602
580
|
# @raise [ArgumentError] if type is not one of: [:friend, :location]
|
603
|
-
|
604
|
-
def list_favorite_things(type, limit:)
|
581
|
+
def list_favorite_things(type)
|
605
582
|
unless [:friend, :location].include? type
|
606
583
|
raise ArgumentError, "Type must be either :friend or :location"
|
607
584
|
end
|
608
585
|
|
609
|
-
raise ArgumentError, "Favorites limit must be positive" if limit < 1
|
610
|
-
|
611
586
|
# Sort the results, with the most favorite thing first.
|
612
587
|
results = instance_variable_get("@#{type}s").sort_by do |thing|
|
613
588
|
-thing.n_activities
|
614
|
-
end
|
615
|
-
|
616
|
-
if results.size == 1
|
617
|
-
favorite = results.first
|
618
|
-
safe_puts "Your favorite #{type} is "\
|
619
|
-
"#{favorite.name} "\
|
620
|
-
"(#{favorite.n_activities} "\
|
621
|
-
"#{favorite.n_activities == 1 ? 'activity' : 'activities'})"
|
622
|
-
else
|
623
|
-
safe_puts "Your favorite #{type}s:"
|
589
|
+
end
|
624
590
|
|
625
|
-
|
591
|
+
@output << "Your favorite #{type}s:"
|
626
592
|
|
627
|
-
|
593
|
+
max_str_size = results.map(&:name).map(&:size).max
|
628
594
|
|
629
|
-
|
630
|
-
first = true
|
631
|
-
data = grouped_results.each.with_object([]) do |(n_activities, things), arr|
|
632
|
-
things.each do |thing|
|
633
|
-
name = thing.name.ljust(max_str_size)
|
634
|
-
if first
|
635
|
-
label = n_activities == 1 ? " activity" : " activities"
|
636
|
-
first = false
|
637
|
-
end
|
638
|
-
str = "#{name} (#{n_activities}#{label})"
|
595
|
+
grouped_results = results.group_by(&:n_activities)
|
639
596
|
|
640
|
-
|
597
|
+
rank = 1
|
598
|
+
first = true
|
599
|
+
data = grouped_results.each.with_object([]) do |(n_activities, things), arr|
|
600
|
+
things.each do |thing|
|
601
|
+
name = thing.name.ljust(max_str_size)
|
602
|
+
if first
|
603
|
+
label = n_activities == 1 ? " activity" : " activities"
|
604
|
+
first = false
|
641
605
|
end
|
642
|
-
|
643
|
-
end
|
606
|
+
str = "#{name} (#{n_activities}#{label})"
|
644
607
|
|
645
|
-
|
646
|
-
# of the numbering prefix because `rank` will simply be the size of all
|
647
|
-
# elements, which may be too large if the last element in the list is a tie.
|
648
|
-
num_str_size = data.last.first.to_s.size + 1 unless data.empty?
|
649
|
-
data.each do |ranking, str|
|
650
|
-
safe_puts "#{"#{ranking}.".ljust(num_str_size)} #{str}"
|
608
|
+
arr << [rank, str]
|
651
609
|
end
|
610
|
+
rank += things.size
|
611
|
+
end
|
612
|
+
|
613
|
+
# We need to use `data.last.first` instead of `rank` to determine the size
|
614
|
+
# of the numbering prefix because `rank` will simply be the size of all
|
615
|
+
# elements, which may be too large if the last element in the list is a tie.
|
616
|
+
num_str_size = data.last.first.to_s.size + 1 unless data.empty?
|
617
|
+
data.each do |ranking, str|
|
618
|
+
@output << "#{"#{ranking}.".ljust(num_str_size)} #{str}"
|
652
619
|
end
|
653
620
|
end
|
654
621
|
|