matthewtodd-events 0.1.0

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.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ Lists upcoming birthdays and anniversaries from your OSX Address Book.
2
+
3
+ I like this better than iCal's "Birthdays" calendar because it goes anywhere I can send STDOUT, and because it figures out how old people are, or what anniversary they've got coming up. (Ah, and iCal doesn't do anniversaries.)
4
+
5
+ So, there you have it.
6
+
7
+ == Usage
8
+
9
+ $ events
10
+ Dino turns 52 TODAY. (Thursday, January 15)
11
+ Fred & Wilma Flintstone's 53rd anniversary is in 5 days. (Tuesday, January 20)
12
+
13
+ == Install
14
+
15
+ gem install matthewtodd-events --source http://gems.github.com
16
+
17
+ == Tips
18
+
19
+ * I like to keep this running in a GeekTool window. If you like, you can bottom-justify this text by piping it through http://gist.github.com/43363.
data/bin/events ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'events'
5
+
6
+ Events::CLI.new(STDOUT, ARGV).run
@@ -0,0 +1,26 @@
1
+ Feature: Display Upcoming Birthdays
2
+ In order to remember to greet my friends and family on their birthdays
3
+ I want to see whose birthdays are coming up soon
4
+
5
+ Scenario: One birthday in the coming week
6
+ Given these people in my address book
7
+ | first_name | last_name | birthday |
8
+ | Matthew | Todd | 1978-04-03 |
9
+ When I ask for upcoming events on 2009-03-31
10
+ Then I should see these results
11
+ """
12
+ Matthew Todd turns 31 in 3 days. (Friday, April 3)
13
+
14
+ """
15
+
16
+ Scenario: One anniversary in the coming week
17
+ Given these people in my address book
18
+ | first_name | last_name | related_names | other_dates |
19
+ | Matthew | Todd | { 'spouse' => 'Valerie Todd' } | { 'anniversary' => '2006-11-04' } |
20
+ | Valerie | Todd | { 'spouse' => 'Matthew Todd' } | { 'anniversary' => '2006-11-04' } |
21
+ When I ask for upcoming events on 2009-11-01
22
+ Then I should see these results
23
+ """
24
+ Matthew & Valerie Todd's 3rd anniversary is in 3 days. (Wednesday, November 4)
25
+
26
+ """
@@ -0,0 +1,20 @@
1
+ require 'test/unit/assertions'
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', '..', 'lib')
3
+ require 'events'
4
+
5
+ World do |world|
6
+ world.extend(Test::Unit::Assertions)
7
+ world
8
+ end
9
+
10
+ Given /^these people in my address book$/ do |table|
11
+ Events.address_book = Events::AddressBook::Fake.new(table.hashes)
12
+ end
13
+
14
+ When /^I ask for upcoming events on (\d\d\d\d-\d\d-\d\d)$/ do |date|
15
+ Events::CLI.new(StringIO.new(@output = '', 'w'), [date]).run
16
+ end
17
+
18
+ Then /^I should see these results$/ do |results|
19
+ assert_equal results, @output
20
+ end
data/lib/events.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'date'
2
+
3
+ require 'events/recurring_date'
4
+ require 'events/person'
5
+ require 'events/address_book'
6
+ require 'events/cli'
7
+
8
+ module Events
9
+ def self.address_book
10
+ @@address_book ||= AddressBook::Real.new
11
+ end
12
+
13
+ def self.address_book=(address_book)
14
+ @@address_book = address_book
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Events
2
+ module AddressBook
3
+ class Base
4
+ def upcoming(date)
5
+ events = []
6
+ @people.each { |person| events.concat(person.upcoming(date)) }
7
+ events.sort_by { |event| event.first }.map { |event| event.last }.uniq
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ require 'events/address_book/fake'
14
+ require 'events/address_book/real'
@@ -0,0 +1,21 @@
1
+ module Events
2
+ module AddressBook
3
+ class Fake < Base
4
+ def initialize(people_attributes = {})
5
+ @people = people_attributes.map { |attributes| Events::Person.new(preprocess(attributes)) }
6
+ end
7
+
8
+ private
9
+
10
+ def preprocess(attributes)
11
+ hashify(attributes, 'related_names')
12
+ hashify(attributes, 'other_dates')
13
+ attributes
14
+ end
15
+
16
+ def hashify(hash, key)
17
+ hash[key] = hash.key?(key) ? eval(hash[key]) : {}
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ # FIXME multiple libruby.dylib found: '/opt/local/lib/libruby.dylib' and '/usr/lib/libruby.1.dylib'
2
+ require 'osx/cocoa'
3
+ OSX.require_framework 'AddressBook'
4
+
5
+ module Events
6
+ module AddressBook
7
+ class Real < Base
8
+ def initialize
9
+ @people = OSX::ABAddressBook.sharedAddressBook.people.to_ruby.map do |card|
10
+ Events::Person.new(attributes(card))
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def attributes(card)
17
+ attributes = {}
18
+ attributes['first_name'] = rubify(card.valueForProperty(OSX::KABFirstNameProperty))
19
+ attributes['last_name'] = rubify(card.valueForProperty(OSX::KABLastNameProperty))
20
+ attributes['nickname'] = rubify(card.valueForProperty(OSX::KABNicknameProperty))
21
+ attributes['birthday'] = rubify(card.valueForProperty(OSX::KABBirthdayProperty))
22
+ attributes['related_names'] = hashify(card.valueForProperty(OSX::KABRelatedNamesProperty))
23
+ attributes['other_dates'] = hashify(card.valueForProperty(OSX::KABOtherDatesProperty))
24
+ attributes
25
+ end
26
+
27
+ def hashify(cocoa_value)
28
+ result = {}
29
+ if cocoa_value
30
+ (0...cocoa_value.count).each do |index|
31
+ result[rubify(cocoa_value.labelAtIndex(index)).downcase.gsub(/[^a-z]/,'')] = rubify(cocoa_value.valueAtIndex(index))
32
+ end
33
+ end
34
+ result
35
+ end
36
+
37
+ def rubify(cocoa_value)
38
+ cocoa_value ? cocoa_value.to_ruby : nil
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/events/cli.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Events
2
+ class CLI
3
+ def initialize(output, argv)
4
+ @output = output
5
+ @date = argv.empty? ? Date.today : Date.parse(argv.shift)
6
+ end
7
+
8
+ def run
9
+ @output.puts Events.address_book.upcoming(@date)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,102 @@
1
+ module Events
2
+ class Person
3
+ def initialize(attributes)
4
+ @first_name = attributes['first_name']
5
+ @last_name = attributes['last_name']
6
+ @nickname = attributes['nickname']
7
+ @birthday = recurring_date(attributes['birthday'])
8
+ @related_names = attributes['related_names']
9
+ @other_dates = recurring_date(attributes['other_dates'])
10
+ end
11
+
12
+ def upcoming(date, within_days=7)
13
+ upcoming = []
14
+
15
+ if @birthday
16
+ next_birthday = @birthday.next_occurrence(date)
17
+ if next_birthday.days_until < within_days
18
+ upcoming << [next_birthday, format(":name_or_nickname turns :age :in_days. (:date)", next_birthday)]
19
+ end
20
+ end
21
+
22
+ if @other_dates['anniversary']
23
+ next_anniversary = @other_dates['anniversary'].next_occurrence(date)
24
+ if next_anniversary.days_until < within_days
25
+ upcoming << [next_anniversary, format(":name_with_spouse's :ordinal_age anniversary is :in_days. (:date)", next_anniversary)]
26
+ end
27
+ end
28
+
29
+ upcoming
30
+ end
31
+
32
+ private
33
+
34
+ def format(string, recurring_date)
35
+ string.sub! /:name_or_nickname/, name_or_nickname
36
+ string.sub! /:name_with_spouse/, name_with_spouse
37
+ string.sub! /:age/, recurring_date.years_since.to_s
38
+ string.sub! /:ordinal_age/, ordinalize(recurring_date.years_since)
39
+ string.sub! /:in_days/, in_days(recurring_date)
40
+ string.sub! /:date/, recurring_date.strftime('%A, %B %e')
41
+ string.strip!
42
+ string.gsub!(/\s+/, ' ')
43
+ string
44
+ end
45
+
46
+ def name
47
+ "#{@first_name} #{@last_name}"
48
+ end
49
+
50
+ def name_or_nickname
51
+ @nickname || name
52
+ end
53
+
54
+ def name_with_spouse
55
+ if spouse_name = @related_names['spouse']
56
+ if spouse_name =~ / #{@last_name}$/
57
+ [@first_name, spouse_name.sub(/ #{@last_name}$/, '')].sort.join(' & ').concat(' ').concat(@last_name)
58
+ else
59
+ [name, spouse_name].sort.join(' & ')
60
+ end
61
+ else
62
+ name
63
+ end
64
+ end
65
+
66
+ def ordinalize(number)
67
+ if (11..13).include?(number.to_i % 100)
68
+ "#{number}th"
69
+ else
70
+ case number.to_i % 10
71
+ when 1; "#{number}st"
72
+ when 2; "#{number}nd"
73
+ when 3; "#{number}rd"
74
+ else "#{number}th"
75
+ end
76
+ end
77
+ end
78
+
79
+ def in_days(recurring_date)
80
+ case in_days = recurring_date.days_until
81
+ when 0; "TODAY"
82
+ when 1; "tomorrow"
83
+ else "in #{in_days} days"
84
+ end
85
+ end
86
+
87
+ def recurring_date(date)
88
+ case date
89
+ when Date, Time, DateTime
90
+ RecurringDate.new(date)
91
+ when String
92
+ RecurringDate.parse(date)
93
+ when Hash
94
+ date.inject({}) { |result, pair| result[pair.first] = recurring_date(pair.last); result }
95
+ when NilClass
96
+ nil
97
+ else
98
+ raise "Unhandled recurring date conversion: #{date.inspect}"
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,46 @@
1
+ module Events
2
+ class RecurringDate
3
+ def self.parse(original)
4
+ original ? new(Date.parse(original)) : nil
5
+ end
6
+
7
+ def initialize(original)
8
+ @original = original
9
+ end
10
+
11
+ def next_occurrence(from = Date.today)
12
+ Occurrence.new(@original, from)
13
+ end
14
+
15
+ private
16
+
17
+ class Occurrence
18
+ include Comparable
19
+
20
+ attr_reader :date
21
+
22
+ def initialize(original, from)
23
+ @date = Date.new(from.year, original.month, original.day)
24
+ @date = Date.new(from.year + 1, original.month, original.day) if @date < from
25
+ @from = from
26
+ @original = original
27
+ end
28
+
29
+ def method_missing(symbol, *args, &block)
30
+ @date.send(symbol, *args, &block)
31
+ end
32
+
33
+ def days_until
34
+ @date - @from
35
+ end
36
+
37
+ def years_since
38
+ @date.year - @original.year
39
+ end
40
+
41
+ def <=>(other)
42
+ self.date <=> other.date
43
+ end
44
+ end
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: matthewtodd-events
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Todd
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-17 00:00:00 -08:00
13
+ default_executable: events
14
+ dependencies: []
15
+
16
+ description:
17
+ email: matthew.todd@gmail.com
18
+ executables:
19
+ - events
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - bin/events
25
+ files:
26
+ - README.rdoc
27
+ - bin/events
28
+ - features/events.feature
29
+ - features/steps/events.rb
30
+ - lib/events/address_book/fake.rb
31
+ - lib/events/address_book/real.rb
32
+ - lib/events/address_book.rb
33
+ - lib/events/cli.rb
34
+ - lib/events/person.rb
35
+ - lib/events/recurring_date.rb
36
+ - lib/events.rb
37
+ has_rdoc: true
38
+ homepage:
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --main
42
+ - README.rdoc
43
+ - --title
44
+ - events-0.1.0
45
+ - --inline-source
46
+ - --line-numbers
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements:
62
+ - RubyCocoa
63
+ rubyforge_project:
64
+ rubygems_version: 1.2.0
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: Lists upcoming birthdays and anniversaries from your OSX Address Book.
68
+ test_files: []
69
+