friends 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: afa08d3f4c3b0c3cf97e75321e94d788ba282d93
4
- data.tar.gz: c0653597da81b0f6132af10ab97faf2358484d91
3
+ metadata.gz: 625e3744b0240347b4e0c74a3a4063b672afab88
4
+ data.tar.gz: 25d066d3bb99d407adb41f01785772f514e7d4fa
5
5
  SHA512:
6
- metadata.gz: a71a226dbad93f72e9cd4a48babe3186ad4c73d7d3483f85ebef94b3053e170ccded2b9ce42873898137b72808d4ef1ce6e6a4ccd6729292c0040ca05a92b099
7
- data.tar.gz: 492a51f2a1abc0eaf8bf533bb2a4f24f7c2dce9a965c4a5cb8d9767658cf7b23be8f64ef86c613dbef27e9a29a8204a9cf342c7c146a0f70a381cb0d155710d6
6
+ metadata.gz: d9e23ab9e130aede68c68e7d65c07de1847585915af9794525db1057c3e3d6939010b73310fa7bf74ef808c94e41f41a41c7555af4d57bf96a1fb66548ee4bcc
7
+ data.tar.gz: 36300571ab2f75ef5406e5a6dddef153370dd5718a08d2dfe262f5ac03b368034ed21f2e6e97d0607abb35880bb4f28feb255e79513bea8c5cc693ce5159f02a
data/.gitignore CHANGED
@@ -13,3 +13,4 @@
13
13
  *.a
14
14
  mkmf.log
15
15
  *.gem
16
+ ideas.txt
@@ -0,0 +1,3 @@
1
+ PreCommit:
2
+ Reek:
3
+ enabled: false
@@ -1,3 +1,6 @@
1
+ AbcSize:
2
+ Enabled: false
3
+
1
4
  AccessorMethodName:
2
5
  Enabled: false
3
6
 
@@ -5,11 +8,6 @@ Alias:
5
8
  Enabled: false
6
9
 
7
10
  AllCops:
8
- Exclude:
9
- - "vendor/**/*"
10
- - "spec/dummy/**/*"
11
- - "db/schema.rb"
12
- - "db/migrate/**/*"
13
11
  RunRailsCops: false
14
12
 
15
13
  AmbiguousOperator:
@@ -181,6 +179,9 @@ ParameterLists:
181
179
  ParenthesesAsGroupedExpression:
182
180
  Enabled: false
183
181
 
182
+ PerceivedComplexity:
183
+ Enabled: false
184
+
184
185
  PercentLiteralDelimiters:
185
186
  PreferredDelimiters:
186
187
  '%': '{}'
data/README.md CHANGED
@@ -1,19 +1,160 @@
1
- [![Code Climate](https://codeclimate.com/github/JacobEvelyn/friends/badges/gpa.svg)](https://codeclimate.com/github/JacobEvelyn/friends) [![Test Coverage](https://codeclimate.com/github/JacobEvelyn/friends/badges/coverage.svg)](https://codeclimate.com/github/JacobEvelyn/friends) [![Build Status](https://travis-ci.org/JacobEvelyn/friends.svg)](https://travis-ci.org/JacobEvelyn/friends)
1
+ [![Code Climate](https://codeclimate.com/github/JacobEvelyn/friends/badges/gpa.svg)](https://codeclimate.com/github/JacobEvelyn/friends) [![Test Coverage](https://codeclimate.com/github/JacobEvelyn/friends/badges/coverage.svg)](https://codeclimate.com/github/JacobEvelyn/friends) [![Build Status](https://travis-ci.org/JacobEvelyn/friends.svg)](https://travis-ci.org/JacobEvelyn/friends) [![Inline docs](http://inch-ci.org/github/JacobEvelyn/friends.png)](http://inch-ci.org/github/JacobEvelyn/friends)
2
2
 
3
3
  # Friends
4
4
 
5
- Spend time with the people you care about. Introvert-tested. Extrovert-approved.
5
+ Spend time with the people you care about. Introvert-tested.
6
+ Extrovert-approved.
7
+
8
+ ### What is it?
9
+
10
+ **Friends** is both a Ruby library and a command-line interface that
11
+ allows you to keep track of your relationships with the people you
12
+ care about.
13
+
14
+ ### Why use it?
15
+
16
+ 1. **Friends** gives you:
17
+ - More organization around staying in touch with friends and
18
+ family.
19
+ - A way to track of the ebbs and flows of your relationships over
20
+ time.
21
+ - Suggestions for who to call or hang out with when you have free
22
+ time, whether it's fifteen minutes or an entire weekend.
23
+ - A low-cost way to record and remember big moments in your life.
24
+ 2. **Friends** stores its data in a universally readable `friends.md`
25
+ Markdown file. No proprietary formats here!
26
+ 3. **Friends** is open-source and very open to new ideas. Contribute!
6
27
 
7
28
  ## Installation
8
29
 
9
- $ gem install friends
30
+ ```
31
+ $ gem install friends
32
+ ```
10
33
 
11
34
  ## Usage
12
35
 
13
- $ friends --help
36
+ ### Basic commands:
37
+
38
+ Add a friend:
39
+
40
+ ```
41
+ $ friends add friend "Grace Hopper"
42
+ Friend added: "Grace Hopper"
43
+ ```
44
+ List your friends:
45
+ ```
46
+ $ friends list friends
47
+ George Washington Carver
48
+ Grace Hopper
49
+ Marie Curie
50
+ ```
51
+ Record an activity with a friend:
52
+ ```
53
+ $ friends add activity "Got lunch with Grace and George."
54
+ Activity added: "2015-01-04: Got lunch with Grace Hopper and George Washington Carver."
55
+ ```
56
+ Or specify a date for the activity:
57
+ ```
58
+ $ friends add activity "2014-12-31: Celebrated the new year with Marie."
59
+ Activity added: "2014-12-31: Celebrated the new year with Marie Curie."
60
+ ```
61
+ List the activities you've recorded:
62
+ ```
63
+ $ friends list activities
64
+ 2015-01-04: Got lunch with Grace Hopper and George Washington Carver.
65
+ 2014-12-31: Celebrated the new year with Marie Curie.
66
+ 2014-11-15: Talked to George Washington Carver on the phone for an hour.
67
+ ```
68
+ Or only list the activities you did with a certain friend:
69
+ ```
70
+ $ friends list activities --with "George"
71
+ 2015-01-04: Got lunch with Grace Hopper and George Washington Carver.
72
+ 2014-11-15: Talked to George Washington Carver on the phone for an hour.
73
+
74
+ ```
75
+ Find your favorite friends:
76
+ ```
77
+ $ friends list favorites
78
+ Your favorite friends:
79
+ 1. George Washington Carver
80
+ 2. Grace Hopper
81
+ 3. Marie Curie
82
+ ```
83
+ Or get a specific number of favorites:
84
+ ```
85
+ $ friends list favorites --limit 2
86
+ Your favorite friends:
87
+ 1. George Washington Carver
88
+ 2. Grace Hopper
89
+ ```
90
+
91
+ ### Global options:
92
+
93
+ ##### --quiet
94
+
95
+ Quiet output messages:
96
+ ```
97
+ $ friends add activity "Went rollerskating with George."
98
+ $ # No output!
99
+
100
+ ```
101
+
102
+ ##### --filename
103
+
104
+ Change the location/name of the `friends.md` file:
105
+ ```
106
+ $ friends --filename ./test/tmp/friends.md clean
107
+ File cleaned: "./test/tmp/friends.md"
108
+ ```
109
+
110
+ ##### --clean
111
+
112
+ Force cleaning of the `friends.md` file, even if the command does not
113
+ normally write to the file.
114
+ ```
115
+ $ friends --clean list friends
116
+ George Washington Carver
117
+ Grace Hopper
118
+ Marie Curie
119
+ File cleaned: "./friends.md"
120
+ ```
121
+
122
+ ### Advanced usage:
123
+
124
+ Wouldn't it be nice to be able to use **Friends** across all of your
125
+ devices? Hooray, you can! Just put the `friends.md` file in your
126
+ Dropbox/Box Sync/Google Drive/whatever folder and use the
127
+ `--filename` flag. You can even set up a Bash/Zsh/whatever alias to
128
+ do this for you, like so:
129
+ ```bash
130
+ alias friends="friends --filename '~/Dropbox/friends.md'"
131
+ ```
132
+
133
+ ### Help:
134
+
135
+ Help menus are available for all levels of commands:
136
+ ```
137
+ $ friends --help
138
+ ```
139
+ ```
140
+ $ friends list --help
141
+ ```
142
+ ```
143
+ $ friends list activities --help
144
+ ```
145
+
146
+ ## Documentation
147
+
148
+ In case you're *really* interested, we have
149
+ [documentation](http://www.rubydoc.info/JacobEvelyn/friends).
14
150
 
15
151
  ## Contributing
16
152
 
153
+ If you have an idea,
154
+ [make a GitHub Issue](https://github.com/JacobEvelyn/friends/issues/new)!
155
+ Suggestions are very very welcome, and often are implemented very
156
+ quickly. And if you'd like to do the implementing yourself:
157
+
17
158
  1. Fork it (https://github.com/JacobEvelyn/friends/fork)
18
159
  2. Create your feature branch (`git checkout -b my-new-feature`)
19
160
  3. Commit your changes (`git commit -am "Add some feature"`)
@@ -21,7 +162,7 @@ Spend time with the people you care about. Introvert-tested. Extrovert-approved.
21
162
  5. Create a new Pull Request
22
163
 
23
164
  **Make sure your changes have appropriate tests (`rake test`) and
24
- conform to the Rubocop style specified. We use
165
+ conform to the Rubocop style specified. This project uses
25
166
  [overcommit](https://github.com/causes/overcommit) to enforce good
26
167
  code.**
27
168
 
@@ -1,5 +1,129 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "friends"
3
+ require "gli"
4
4
 
5
- Friends::Extrovert.start(ARGV)
5
+ require "friends/introvert"
6
+ require "friends/version"
7
+
8
+ include GLI::App
9
+
10
+ program_desc "Spend time with the people you care about. Introvert-tested. "\
11
+ "Extrovert-approved."
12
+
13
+ version Friends::VERSION
14
+
15
+ subcommand_option_handling :normal
16
+ arguments :strict
17
+
18
+ switch [:quiet],
19
+ negatable: false,
20
+ desc: "Quiet output messages"
21
+
22
+ flag [:filename],
23
+ arg_name: "FILENAME",
24
+ default_value: "./friends.md",
25
+ desc: "Set the location of the friends file"
26
+
27
+ switch [:clean],
28
+ negatable: false,
29
+ desc: "Force a clean write of the friends file"
30
+
31
+ desc "Lists friends or activities"
32
+ command :list do |c|
33
+ c.desc "List all friends"
34
+ c.command :friends do |list_friends|
35
+ list_friends.action do
36
+ puts @introvert.list_friends
37
+ end
38
+ end
39
+
40
+ c.desc "List favorite friends"
41
+ c.command :favorites do |list_favorites|
42
+ list_favorites.flag [:limit],
43
+ arg_name: "NUMBER",
44
+ default_value: 10,
45
+ desc: "The number of friends to return"
46
+
47
+ list_favorites.action do |_, options|
48
+ limit = options[:limit].to_i
49
+ favorites = @introvert.list_favorites(limit: limit)
50
+
51
+ if limit == 1
52
+ puts "Your best friend is #{favorites.first}"
53
+ else
54
+ puts "Your favorite friends:"
55
+ num = 0
56
+ favorites.each { |name| puts "#{"#{num += 1}.".ljust(2)} #{name}" }
57
+ end
58
+ end
59
+ end
60
+
61
+ c.desc "Lists all activities"
62
+ c.command :activities do |list_activities|
63
+ list_activities.flag [:with],
64
+ arg_name: "NAME",
65
+ desc: "List only activities involving the given friend"
66
+
67
+ list_activities.action do |_, options|
68
+ puts @introvert.list_activities(with: options[:with])
69
+ end
70
+ end
71
+ end
72
+
73
+ desc "Adds a friend or activity"
74
+ command :add do |c|
75
+ c.desc "Adds a friend"
76
+ c.arg_name "NAME"
77
+ c.command :friend do |add_friend|
78
+ add_friend.action do |_, _, args|
79
+ friend = @introvert.add_friend(name: args.first)
80
+ @message = "Friend added: \"#{friend.name}\""
81
+ end
82
+ end
83
+
84
+ c.desc "Adds an activity"
85
+ c.arg_name "DESCRIPTION"
86
+ c.command :activity do |add_activity|
87
+ add_activity.action do |_, _, args|
88
+ activity = @introvert.add_activity(serialization: args.first)
89
+ @message = "Activity added: \"#{activity.display_text}\""
90
+ end
91
+ end
92
+ end
93
+
94
+ desc "Cleans your friends.md file"
95
+ command :clean do |c|
96
+ c.action do
97
+ filename = @introvert.clean
98
+ @message = "File cleaned: \"#{filename}\""
99
+ end
100
+ end
101
+
102
+ # Before each command, clean up all arguments and create the global Introvert.
103
+ pre do |global_options, _, options|
104
+ final_options = global_options.merge!(options).select do |key, _|
105
+ [:filename].include? key
106
+ end
107
+
108
+ @introvert = Friends::Introvert.new(final_options)
109
+ true
110
+ end
111
+
112
+ post do |global_options|
113
+ # After each command, clean if requested with the --clean flag.
114
+ if global_options[:clean]
115
+ filename = @introvert.clean
116
+ @message = "File cleaned: \"#{filename}\""
117
+ end
118
+
119
+ # Print the output message (if there is one) unless --quiet is passed.
120
+ puts @message unless @message.nil? || global_options[:quiet]
121
+ end
122
+
123
+ # If an error is raised, print the message to STDERR and exit the program.
124
+ on_error do |error|
125
+ abort "Error: #{error}"
126
+ end
127
+
128
+ # Run the program and return the exit code corresponding to the its success.
129
+ exit run(ARGV)
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["jacobevelyn@gmail.com"]
11
11
  spec.summary = %q{Spend time with the people you care about.}
12
12
  spec.description = %q{Spend time with the people you care about. Introvert-tested. Extrovert-approved.}
13
- spec.homepage = ""
13
+ spec.homepage = "https://github.com/JacobEvelyn/friends"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -18,9 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "thor"
21
+ spec.add_dependency "gli", "~> 2.12"
22
+ spec.add_dependency "memoist", "~> 0.11"
22
23
 
23
24
  spec.add_development_dependency "rake", "~> 10.0"
24
25
  spec.add_development_dependency "bundler", "~> 1.7"
25
- spec.add_development_dependency "codeclimate-test-reporter"
26
+ spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4"
26
27
  end
data/friends.md CHANGED
@@ -1,3 +1,9 @@
1
+ ### Activities:
2
+ - 2015-01-04: Got lunch with **Grace Hopper** and **George Washington Carver**.
3
+ - 2014-12-31: Celebrated the new year with **Marie Curie**.
4
+ - 2014-11-15: Talked to **George Washington Carver** on the phone for an hour.
5
+
1
6
  ### Friends:
2
- - Ben Yavitt
3
- - Zane
7
+ - George Washington Carver
8
+ - Grace Hopper
9
+ - Marie Curie
@@ -1,4 +1,5 @@
1
- require "friends/extrovert"
1
+ require "friends/introvert"
2
+ require "friends/version"
2
3
 
3
4
  module Friends
4
5
  end
@@ -0,0 +1,127 @@
1
+ # Activity represents an activity you've done with one or more Friends.
2
+
3
+ require "memoist"
4
+
5
+ require "friends/serializable"
6
+
7
+ module Friends
8
+ class Activity
9
+ extend Serializable
10
+ extend Memoist
11
+
12
+ SERIALIZATION_PREFIX = "- "
13
+
14
+ # @return [Regexp] the regex for capturing groups in deserialization
15
+ def self.deserialization_regex
16
+ /(#{SERIALIZATION_PREFIX})?((?<date_s>\d{4}-\d\d-\d\d):\s)?(?<description>.+)/
17
+ end
18
+
19
+ # @return [Regexp] the string of what we expected during deserialization
20
+ def self.deserialization_expectation
21
+ "[YYYY-MM-DD]: [Activity]"
22
+ end
23
+
24
+ # @param date_s [String] the activity's date, parsed using Date.parse()
25
+ # @param description [String] the activity's description
26
+ # @return [Activity] the new activity
27
+ def initialize(date_s: Date.today.to_s, description:)
28
+ @date = Date.parse(date_s)
29
+ @description = description
30
+ end
31
+
32
+ attr_reader :date
33
+ attr_reader :description
34
+
35
+ # @return [String] the command-line display text for the activity
36
+ def display_text
37
+ date_s = "\e[1m#{date}\e[0m"
38
+ description_s = description
39
+ while match = description_s.match(/(\*\*)([^\*]+)(\*\*)/)
40
+ description_s =
41
+ "#{match.pre_match}\e[1m#{match[2]}\e[0m#{match.post_match}"
42
+ end
43
+ "#{date_s}: #{description_s}"
44
+ end
45
+
46
+ # @return [String] the file serialization text for the activity
47
+ def serialize
48
+ "#{SERIALIZATION_PREFIX}#{date}: #{description}"
49
+ end
50
+
51
+ # Modify the description to turn inputted friend names
52
+ # (e.g. "Jacob" or "Jacob Evelyn") into full asterisk'd names
53
+ # (e.g. "**Jacob Evelyn**")
54
+ # @param friends [Array] list of friends to highlight in the description
55
+ # @raise [FriendsError] if more than one friend matches a part of the
56
+ # description
57
+ def highlight_friends(friends:)
58
+ # Map each friend to a list of all possible regexes for that friend.
59
+ friend_regexes = {}
60
+ friends.each { |f| friend_regexes[f.name] = regexes_for_name(f.name) }
61
+
62
+ # Create hash mapping regex to friend name. Note that because two friends
63
+ # may have the same regex (e.g. /John/), we need to store the names in an
64
+ # array since there may be more than one. We also iterate through the
65
+ # regexes to add the most important regexes to the hash first, so
66
+ # "Jacob Evelyn" takes precedence over all instances of "Jacob" (since
67
+ # Ruby hashes are ordered).
68
+ regex_map = Hash.new { |h, k| h[k] = [] }
69
+ while !friend_regexes.empty?
70
+ friend_regexes.each do |friend_name, regex_list|
71
+ regex_map[regex_list.shift] << friend_name
72
+ friend_regexes.delete(friend_name) if regex_list.empty?
73
+ end
74
+ end
75
+
76
+ # Go through the description and substitute in full, asterisk'd names for
77
+ # anything that matches a friend's name.
78
+ new_description = description.clone
79
+ regex_map.each do |regex, names|
80
+ new_description.gsub!(regex, "**#{names.first}**") if names.size == 1
81
+ end
82
+
83
+ @description = new_description
84
+ end
85
+
86
+ # Find the names of all friends in this description.
87
+ # @return [Array] list of all friend names in the description
88
+ def friend_names
89
+ description.scan(/(?<=\*\*)\w[^\*]*(?=\*\*)/).uniq
90
+ end
91
+ memoize :friend_names
92
+
93
+ private
94
+
95
+ # Default sorting for an array of activities is reverse-date.
96
+ def <=>(other)
97
+ other.date <=> date
98
+ end
99
+
100
+ # @return [Array] a list of all regexes to match the name in a string, with
101
+ # longer regexes first
102
+ # Note: for now we only match on full names or first names
103
+ # Example: [
104
+ # /Jacob\s+Morris\s+Evelyn/,
105
+ # /Jacob/
106
+ # ]
107
+ def regexes_for_name(name)
108
+ # We generously allow any amount of whitespace between parts of a name.
109
+ splitter = "\\s+"
110
+
111
+ # We don't want to match names that are directly touching double asterisks
112
+ # as these are treated as sacred by our program.
113
+ no_leading_asterisks = "(?<!\\*\\*)"
114
+ no_ending_asterisks = "(?!\\*\\*)"
115
+
116
+ # Create the list of regexes and return it.
117
+ chunks = name.split(Regexp.new(splitter))
118
+
119
+ [chunks, [chunks.first]].map do |words|
120
+ Regexp.new(
121
+ no_leading_asterisks + words.join(splitter) + no_ending_asterisks,
122
+ Regexp::IGNORECASE
123
+ )
124
+ end
125
+ end
126
+ end
127
+ end