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 +19 -0
- data/bin/events +6 -0
- data/features/events.feature +26 -0
- data/features/steps/events.rb +20 -0
- data/lib/events.rb +16 -0
- data/lib/events/address_book.rb +14 -0
- data/lib/events/address_book/fake.rb +21 -0
- data/lib/events/address_book/real.rb +42 -0
- data/lib/events/cli.rb +12 -0
- data/lib/events/person.rb +102 -0
- data/lib/events/recurring_date.rb +46 -0
- metadata +69 -0
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,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,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
|
+
|