friends 0.0.1 → 0.0.2

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