stamp 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +2 -1
- data/CONTRIBUTING.md +11 -0
- data/Gemfile +0 -5
- data/README.md +16 -40
- data/features/stamp.feature +13 -12
- data/features/step_definitions/stamp_steps.rb +5 -1
- data/lib/stamp.rb +26 -29
- data/lib/stamp/emitters/am_pm.rb +22 -0
- data/lib/stamp/emitters/composite.rb +31 -0
- data/lib/stamp/emitters/delegate.rb +19 -0
- data/lib/stamp/emitters/lookup.rb +27 -0
- data/lib/stamp/emitters/modifiable.rb +9 -0
- data/lib/stamp/emitters/ordinal.rb +31 -0
- data/lib/stamp/emitters/string.rb +23 -0
- data/lib/stamp/emitters/two_digit.rb +22 -0
- data/lib/stamp/translator.rb +66 -83
- data/lib/stamp/version.rb +1 -1
- data/stamp.gemspec +3 -0
- metadata +46 -6
- data/Gemfile.lock +0 -27
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
## Contributing to stamp
|
2
|
+
|
3
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
4
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
5
|
+
* Fork the project
|
6
|
+
* Run `bundle install`
|
7
|
+
* Run `rake` to execute the cucumber specs and make sure they all pass
|
8
|
+
* Start a feature/bugfix branch
|
9
|
+
* Commit and push until you are happy with your contribution
|
10
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
11
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# stamp
|
2
2
|
|
3
3
|
Format dates and times based on human-friendly examples, not arcane
|
4
|
-
strftime directives.
|
4
|
+
[strftime](http://strfti.me) directives.
|
5
5
|
|
6
6
|
[![Build Status](https://secure.travis-ci.org/jeremyw/stamp.png)](http://travis-ci.org/jeremyw/stamp)
|
7
7
|
|
@@ -15,7 +15,7 @@ Your Ruby dates and times get a powerful new method: `stamp`.
|
|
15
15
|
|
16
16
|
You might be concerned that "stamp" isn't descriptive enough for developers
|
17
17
|
reading your code who aren't familiar with this gem. If that's the case, the
|
18
|
-
following aliases are
|
18
|
+
following aliases are provided:
|
19
19
|
|
20
20
|
* `stamp_like`
|
21
21
|
* `format_like`
|
@@ -27,11 +27,11 @@ and weekday parts you'd like, and your date will be formatted accordingly:
|
|
27
27
|
|
28
28
|
```ruby
|
29
29
|
date = Date.new(2011, 6, 9)
|
30
|
-
date.stamp("March 1, 1999") #=> "June
|
31
|
-
date.stamp("Jan 1, 1999") #=> "Jun
|
30
|
+
date.stamp("March 1, 1999") #=> "June 9, 2011"
|
31
|
+
date.stamp("Jan 1, 1999") #=> "Jun 9, 2011"
|
32
32
|
date.stamp("Jan 01") #=> "Jun 09"
|
33
|
-
date.stamp("Sunday, May 1, 2000") #=> "Thursday, June
|
34
|
-
date.stamp("Sun Aug 5") #=> "Thu Jun
|
33
|
+
date.stamp("Sunday, May 1, 2000") #=> "Thursday, June 9, 2011"
|
34
|
+
date.stamp("Sun Aug 5") #=> "Thu Jun 9"
|
35
35
|
date.stamp("12/31/99") #=> "06/09/11"
|
36
36
|
date.stamp("DOB: 12/31/2000") #=> "DOB: 06/09/2011"
|
37
37
|
```
|
@@ -50,11 +50,11 @@ hours, minutes, and seconds when it sees colon-separated values.
|
|
50
50
|
|
51
51
|
```ruby
|
52
52
|
time = Time.utc(2011, 6, 9, 20, 52, 30)
|
53
|
-
time.stamp("3:00 AM") #=> "
|
53
|
+
time.stamp("3:00 AM") #=> "8:52 PM"
|
54
54
|
time.stamp("01:00:00 AM") #=> "08:52:30 PM"
|
55
55
|
time.stamp("23:59") #=> "20:52"
|
56
56
|
time.stamp("23:59:59") #=> "20:52:30"
|
57
|
-
time.stamp("Jan 1 at 01:00 AM") #=> "Jun
|
57
|
+
time.stamp("Jan 1 at 01:00 AM") #=> "Jun 9 at 08:52 PM"
|
58
58
|
time.stamp("23:59 UTC") #=> "20:52 PST"
|
59
59
|
```
|
60
60
|
|
@@ -76,53 +76,29 @@ For example, "01/09" could refer to January 9, September 1, or
|
|
76
76
|
January 2009. More explicit examples include "12/31", "31/12", and "12/99".
|
77
77
|
|
78
78
|
Using unambiguous values will also help people who read the code in the
|
79
|
-
future understand your intent.
|
79
|
+
future, including yourself, understand your intent.
|
80
80
|
|
81
81
|
### Rails Integration
|
82
82
|
|
83
|
-
Stamp makes it easy to configure your application's common date and time
|
84
|
-
formats in a more self-documenting way with
|
83
|
+
Stamp makes it easy to configure your Rails application's common date and time
|
84
|
+
formats in a more self-documenting way with `DATE_FORMATS`:
|
85
85
|
|
86
86
|
```ruby
|
87
|
-
# config/initializers/
|
88
|
-
Date::DATE_FORMATS[:short] =
|
89
|
-
Time::DATE_FORMATS[:military] =
|
87
|
+
# config/initializers/date_formats.rb
|
88
|
+
Date::DATE_FORMATS[:short] = Proc.new { |date| date.stamp("Sun Jan 5") }
|
89
|
+
Time::DATE_FORMATS[:military] = Proc.new { |time| time.stamp("5 January 23:59") }
|
90
90
|
```
|
91
91
|
|
92
92
|
To use your formats:
|
93
93
|
|
94
94
|
```ruby
|
95
95
|
Date.today.to_s(:short) #=> "Sat Jul 16"
|
96
|
-
Time.now.to_s(:military) #=> "15:35"
|
96
|
+
Time.now.to_s(:military) #=> "16 July 15:35"
|
97
97
|
```
|
98
98
|
|
99
99
|
### Limitations
|
100
100
|
|
101
|
-
* DateTime should inherit stamp behavior from Date
|
102
|
-
|
103
|
-
### Advanced Usage
|
104
|
-
|
105
|
-
If you need more obscure formatting options, you can include any valid
|
106
|
-
[strftime](http://strfti.me) directives in your example string, and they'll
|
107
|
-
just be passed along:
|
108
|
-
|
109
|
-
```ruby
|
110
|
-
Date.today.stamp("Week #%U, 1999") #=> "Week #23, 2011"
|
111
|
-
```
|
112
|
-
|
113
|
-
Check out [http://strfti.me](http://strfti.me) for more ideas.
|
114
|
-
|
115
|
-
## Contributing to stamp
|
116
|
-
|
117
|
-
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
118
|
-
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
119
|
-
* Fork the project
|
120
|
-
* Run `bundle install`
|
121
|
-
* Run `rake` to execute the cucumber specs and make sure they all pass
|
122
|
-
* Start a feature/bugfix branch
|
123
|
-
* Commit and push until you are happy with your contribution
|
124
|
-
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
125
|
-
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
101
|
+
* `DateTime` should inherit stamp behavior from `Date`, but it hasn't been thoroughly tested. Patches welcome!
|
126
102
|
|
127
103
|
## Copyright
|
128
104
|
|
data/features/stamp.feature
CHANGED
@@ -14,15 +14,15 @@ Feature: Stamping a date
|
|
14
14
|
| example | output |
|
15
15
|
| January | September |
|
16
16
|
| Jan | Sep |
|
17
|
-
| Jan 1 | Sep
|
17
|
+
| Jan 1 | Sep 8 |
|
18
18
|
| Jan 01 | Sep 08 |
|
19
19
|
| Jan 10 | Sep 08 |
|
20
|
-
| Jan 1, 1999 | Sep
|
20
|
+
| Jan 1, 1999 | Sep 8, 2011 |
|
21
21
|
| Jan 12, 1999 | Sep 08, 2011 |
|
22
22
|
| 13 January 1999 | 08 September 2011 |
|
23
23
|
| Monday | Thursday |
|
24
|
-
| Tue, Jan 1 | Thu, Sep
|
25
|
-
| Tuesday, January 1, 1999 | Thursday, September
|
24
|
+
| Tue, Jan 1 | Thu, Sep 8 |
|
25
|
+
| Tuesday, January 1, 1999 | Thursday, September 8, 2011 |
|
26
26
|
| 01/1999 | 09/2011 |
|
27
27
|
| 01/01 | 09/08 |
|
28
28
|
| 01/31 | 09/08 |
|
@@ -67,7 +67,8 @@ Feature: Stamping a date
|
|
67
67
|
|
68
68
|
@time
|
69
69
|
Scenario Outline: Formatting times by example
|
70
|
-
Given the time
|
70
|
+
Given the time zone is "EST"
|
71
|
+
And the time February 8, 2011 at 13:31:27
|
71
72
|
When I stamp the example "<example>"
|
72
73
|
Then I produce "<output>"
|
73
74
|
|
@@ -82,7 +83,7 @@ Feature: Stamping a date
|
|
82
83
|
| 08:59:59 AM | 01:31:27 PM |
|
83
84
|
| 08:59:59 PM | 01:31:27 PM |
|
84
85
|
| 23:59:59 | 13:31:27 |
|
85
|
-
| 8:59 PST | 1:31
|
86
|
+
| 8:59 PST | 1:31 EST |
|
86
87
|
|
87
88
|
@date
|
88
89
|
@time
|
@@ -93,19 +94,19 @@ Feature: Stamping a date
|
|
93
94
|
|
94
95
|
Examples:
|
95
96
|
| example | output |
|
96
|
-
| Jan 1, 1999 8:59 am | Sep
|
97
|
+
| Jan 1, 1999 8:59 am | Sep 8, 2011 1:31 pm |
|
97
98
|
| 08:59 AM 1999-12-31 | 01:31 PM 2011-09-08 |
|
98
|
-
| Date: Jan 1, 1999 Time: 8:59 am | Date: Sep
|
99
|
+
| Date: Jan 1, 1999 Time: 8:59 am | Date: Sep 8, 2011 Time: 1:31 pm |
|
99
100
|
|
100
101
|
Scenario: strftime directives just get passed through
|
101
102
|
Given the date December 21, 2012
|
102
|
-
When I stamp the example "John Cusack was in a movie about
|
103
|
-
Then I produce "John Cusack was in a movie about Dec
|
103
|
+
When I stamp the example "John Cusack was in a movie about Jan (%-m) %e, %Y, but it wasn't very good."
|
104
|
+
Then I produce "John Cusack was in a movie about Dec (%-m) %e, %Y, but it wasn't very good."
|
104
105
|
|
105
106
|
Scenario: Plain text just gets passed through
|
106
107
|
Given the date June 1, 1926
|
107
|
-
When I stamp the example "Marilyn Monroe was born on January
|
108
|
-
Then I produce "Marilyn Monroe was born on June
|
108
|
+
When I stamp the example "Marilyn Monroe was born on January 9, 1999."
|
109
|
+
Then I produce "Marilyn Monroe was born on June 1, 1926."
|
109
110
|
|
110
111
|
Scenario Outline: Aliases for the stamp method
|
111
112
|
Given the date December 9, 2011
|
@@ -10,7 +10,11 @@ Given /^the date (\w+) (\d+), (\d{4})$/ do |month_name, day, year|
|
|
10
10
|
end
|
11
11
|
|
12
12
|
Given /^the time (\w+) (\d+), (\d+) at (\d{2}):(\d{2}):(\d{2})$/ do |month_name, day, year, hours, minutes, seconds|
|
13
|
-
@target = Time.
|
13
|
+
@target = Time.local(year.to_i, month(month_name), day.to_i, hours.to_i, minutes.to_i, seconds.to_i)
|
14
|
+
end
|
15
|
+
|
16
|
+
Given /^the time zone is "(.*?)"$/ do |zone|
|
17
|
+
ENV['TZ'] = zone
|
14
18
|
end
|
15
19
|
|
16
20
|
When /^I stamp the example "([^"]*)"$/ do |example|
|
data/lib/stamp.rb
CHANGED
@@ -1,53 +1,50 @@
|
|
1
1
|
require "date"
|
2
2
|
require "time"
|
3
3
|
|
4
|
+
require "stamp/emitters/modifiable"
|
5
|
+
require "stamp/emitters/am_pm"
|
6
|
+
require "stamp/emitters/composite"
|
7
|
+
require "stamp/emitters/delegate"
|
8
|
+
require "stamp/emitters/lookup"
|
9
|
+
require "stamp/emitters/ordinal"
|
10
|
+
require "stamp/emitters/string"
|
11
|
+
require "stamp/emitters/two_digit"
|
4
12
|
require "stamp/translator"
|
5
13
|
require "stamp/version"
|
6
14
|
|
7
15
|
module Stamp
|
8
|
-
|
9
|
-
#
|
10
|
-
|
11
|
-
#
|
12
|
-
# @param [String] example a human-friendly date/time example
|
13
|
-
# @param [#strftime] the Date or Time to be formatted. Optional, but may
|
14
|
-
# be used to support certain edge cases
|
15
|
-
# @return [String] a strftime-friendly format
|
16
|
-
#
|
17
|
-
# @example
|
18
|
-
# Stamp.strftime_format("Jan 1, 1999") #=> "%b %e, %Y"
|
19
|
-
def self.strftime_format(example, target=nil)
|
20
|
-
Stamp::StrftimeTranslator.new(target).translate(example)
|
21
|
-
end
|
16
|
+
# Limits the number of formats that we memoize to prevent unbounded
|
17
|
+
# memory consumption.
|
18
|
+
MEMOIZATION_CAP = 999
|
22
19
|
|
23
20
|
# Formats a date/time using a human-friendly example as a template.
|
24
21
|
#
|
25
|
-
# @param
|
22
|
+
# @param [String] example a human-friendly date/time example
|
23
|
+
# @param [Hash] options
|
24
|
+
# @option options [Boolean] :memoize (true)
|
25
|
+
#
|
26
26
|
# @return [String] the formatted date or time
|
27
27
|
#
|
28
28
|
# @example
|
29
29
|
# Date.new(2012, 12, 21).stamp("Jan 1, 1999") #=> "Dec 21, 2012"
|
30
30
|
def stamp(example)
|
31
|
-
|
31
|
+
memoize_stamp_emitters(example).format(self)
|
32
32
|
end
|
33
33
|
alias :stamp_like :stamp
|
34
34
|
alias :format_like :stamp
|
35
35
|
|
36
|
-
#
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
# @example
|
43
|
-
# Date.today.strftime_format("Jan 1, 1999") #=> "%b %e, %Y"
|
44
|
-
def strftime_format(example)
|
45
|
-
# delegate to the class method, providing self as a target value to
|
46
|
-
# support certain edge cases
|
47
|
-
Stamp.strftime_format(example, self)
|
36
|
+
# Memoizes the set of emitter objects for the given +example+ in
|
37
|
+
# order to improve performance.
|
38
|
+
def memoize_stamp_emitters(example)
|
39
|
+
@@memoized_stamp_emitters ||= {}
|
40
|
+
@@memoized_stamp_emitters.clear if @@memoized_stamp_emitters.size > MEMOIZATION_CAP
|
41
|
+
@@memoized_stamp_emitters[example] ||= stamp_emitters(example)
|
48
42
|
end
|
49
43
|
|
44
|
+
def stamp_emitters(example)
|
45
|
+
Translator.new.translate(example)
|
46
|
+
end
|
50
47
|
end
|
51
48
|
|
52
49
|
Date.send(:include, ::Stamp)
|
53
|
-
Time.send(:include, ::Stamp)
|
50
|
+
Time.send(:include, ::Stamp)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Stamp
|
2
|
+
module Emitters
|
3
|
+
class AmPm
|
4
|
+
include Modifiable
|
5
|
+
|
6
|
+
AM = 'am'
|
7
|
+
PM = 'pm'
|
8
|
+
|
9
|
+
def initialize(&block)
|
10
|
+
@modifier = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def format(target)
|
14
|
+
modify(target.hour < 12 ? AM : PM)
|
15
|
+
end
|
16
|
+
|
17
|
+
def field
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Stamp
|
2
|
+
module Emitters
|
3
|
+
class Composite
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@emitters = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(target)
|
11
|
+
# NOTE using #each to build string because benchmarking shows
|
12
|
+
# that it's ~20% faster than .map.join('')
|
13
|
+
result = ''
|
14
|
+
@emitters.each { |e| result << e.format(target).to_s }
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(emitter)
|
19
|
+
if emitter.is_a?(Enumerable)
|
20
|
+
emitter.each { |e| @emitters << e }
|
21
|
+
else
|
22
|
+
@emitters << emitter
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each(&block)
|
27
|
+
@emitters.each(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Stamp
|
2
|
+
module Emitters
|
3
|
+
class Delegate
|
4
|
+
include Modifiable
|
5
|
+
|
6
|
+
attr_reader :field
|
7
|
+
|
8
|
+
# @param [field] the field to be formatted (e.g. +:month+, +:year+)
|
9
|
+
def initialize(field, &block)
|
10
|
+
@field = field
|
11
|
+
@modifier = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def format(target)
|
15
|
+
modify(target.send(field))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Stamp
|
2
|
+
module Emitters
|
3
|
+
class Lookup
|
4
|
+
attr_reader :field
|
5
|
+
|
6
|
+
# @param [field] the field to be formatted (e.g. +:month+, +:year+)
|
7
|
+
# @param [lookup] an array of the string values to be formatted (e.g. +Date::DAYNAMES+)
|
8
|
+
# or a +call+able that returns the formatted value
|
9
|
+
def initialize(field, lookup=nil)
|
10
|
+
@field = field
|
11
|
+
@lookup = lookup
|
12
|
+
end
|
13
|
+
|
14
|
+
def format(target)
|
15
|
+
lookup(target.send(field))
|
16
|
+
end
|
17
|
+
|
18
|
+
def lookup(value)
|
19
|
+
if @lookup.respond_to?(:call)
|
20
|
+
@lookup.call(value)
|
21
|
+
else
|
22
|
+
@lookup[value]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Stamp
|
2
|
+
module Emitters
|
3
|
+
class Ordinal
|
4
|
+
attr_reader :field
|
5
|
+
|
6
|
+
# @param [field] the field to be formatted (e.g. +:month+, +:year+)
|
7
|
+
def initialize(field)
|
8
|
+
@field = field
|
9
|
+
end
|
10
|
+
|
11
|
+
def format(target)
|
12
|
+
ordinalize(target.send(field))
|
13
|
+
end
|
14
|
+
|
15
|
+
# Cribbed from ActiveSupport::Inflector
|
16
|
+
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb
|
17
|
+
def ordinalize(number)
|
18
|
+
number.to_s + if (11..13).include?(number % 100)
|
19
|
+
'th'
|
20
|
+
else
|
21
|
+
case number % 10
|
22
|
+
when 1; 'st'
|
23
|
+
when 2; 'nd'
|
24
|
+
when 3; 'rd'
|
25
|
+
else 'th'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Stamp
|
2
|
+
module Emitters
|
3
|
+
class String
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
def initialize(value)
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(target)
|
11
|
+
value
|
12
|
+
end
|
13
|
+
|
14
|
+
def <<(emitter)
|
15
|
+
value << emitter.value
|
16
|
+
end
|
17
|
+
|
18
|
+
def field
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Stamp
|
2
|
+
module Emitters
|
3
|
+
# Emits the given field as a two-digit number with a leading
|
4
|
+
# zero if necessary.
|
5
|
+
class TwoDigit
|
6
|
+
include Modifiable
|
7
|
+
|
8
|
+
attr_reader :field
|
9
|
+
|
10
|
+
# @param [field] the field to be formatted (e.g. +:month+, +:year+)
|
11
|
+
def initialize(field, &block)
|
12
|
+
@field = field
|
13
|
+
@modifier = block
|
14
|
+
end
|
15
|
+
|
16
|
+
def format(target)
|
17
|
+
value = modify(target.send(field))
|
18
|
+
value < 10 ? "0#{value}" : value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/stamp/translator.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
module Stamp
|
2
|
-
class
|
2
|
+
class Translator
|
3
|
+
|
4
|
+
# Full list of time zone abbreviations from
|
5
|
+
# http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
|
6
|
+
TIME_ZONE_ABBREVIATIONS = %w{
|
7
|
+
ACDT ACST ACT ADT AEDT AEST AFT AKDT AKST AMST AMT ART AST AWDT AWST AZOST AZT
|
8
|
+
BDT BIOT BIT BOT BRT BST BTTCAT CCT CDT CEDT CEST CET CHADT CHAST CHOT ChST CHUT
|
9
|
+
CIST CIT CKT CLST CLT COST COT CST CT CVT CWST CXT DAVT DDUT DFT EASST EAST EAT
|
10
|
+
ECT EDT EEDT EEST EET EGST EGT EIT EST FET FJT FKST FKT FNT GALT GAMT GET GFT
|
11
|
+
GILT GIT GMT GST GYT HADT HAEC HAST HKT HMT HOVT HST ICT IDT IOT IRDT IRKT IRST
|
12
|
+
IST JST KGT KOST KRAT KST LHST LINT MAGT MART MAWT MDT MET MEST MHT MIST MIT MMT
|
13
|
+
MSK MST MUT MVT MYT NCT NDT NFT NPT NST NT NUT NZDT NZST OMST ORAT PDT PET PETT
|
14
|
+
PGT PHOT PHT PKT PMDT PMST PONT PST RET ROTT SAKT SAMT SAST SBT SCT SGT SLT SRT
|
15
|
+
SST SYOT TAHT THA TFT TJT TKT TLT TMT TOT TVT UCT ULAT UTC UYST UYT UZT VET VLAT
|
16
|
+
VOLT VOST VUT WAKT WAST WAT WEDT WEST WET WST YAKT YEKT
|
17
|
+
}
|
18
|
+
|
19
|
+
TIMEZONE_REGEXP = /^(#{TIME_ZONE_ABBREVIATIONS.join('|')})$/
|
3
20
|
MONTHNAMES_REGEXP = /^(#{Date::MONTHNAMES.compact.join('|')})$/i
|
4
21
|
ABBR_MONTHNAMES_REGEXP = /^(#{Date::ABBR_MONTHNAMES.compact.join('|')})$/i
|
5
22
|
DAYNAMES_REGEXP = /^(#{Date::DAYNAMES.join('|')})$/i
|
6
23
|
ABBR_DAYNAMES_REGEXP = /^(#{Date::ABBR_DAYNAMES.join('|')})$/i
|
7
24
|
|
8
|
-
# Full list of time zone abbreviations from http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
|
9
|
-
TIMEZONE_REGEXP = /^(ACDT|ACST|ACT|ADT|AEDT|AEST|AFT|AKDT|AKST|AMST|AMT|ART|AST|AST|AST|AST|AWDT|AWST|AZOST|AZT|BDT|BIOT|BIT|BOT|BRT|BST|BST|BTT|CAT|CCT|CDT|CDT|CEDT|CEST|CET|CHADT|CHAST|CHOT|ChST|CHUT|CIST|CIT|CKT|CLST|CLT|COST|COT|CST|CST|CST|CST|CST|CT|CVT|CWST|CXT|DAVT|DDUT|DFT|EASST|EAST|EAT|ECT|ECT|EDT|EEDT|EEST|EET|EGST|EGT|EIT|EST|EST|FET|FJT|FKST|FKT|FNT|GALT|GAMT|GET|GFT|GILT|GIT|GMT|GST|GST|GYT|HADT|HAEC|HAST|HKT|HMT|HOVT|HST|ICT|IDT|IOT|IRDT|IRKT|IRST|IST|IST|IST|JST|KGT|KOST|KRAT|KST|LHST|LHST|LINT|MAGT|MART|MAWT|MDT|MET|MEST|MHT|MIST|MIT|MMT|MSK|MST|MST|MST|MUT|MVT|MYT|NCT|NDT|NFT|NPT|NST|NT|NUT|NZDT|NZST|OMST|ORAT|PDT|PET|PETT|PGT|PHOT|PHT|PKT|PMDT|PMST|PONT|PST|PST|RET|ROTT|SAKT|SAMT|SAST|SBT|SCT|SGT|SLT|SRT|SST|SST|SYOT|TAHT|THA|TFT|TJT|TKT|TLT|TMT|TOT|TVT|UCT|ULAT|UTC|UYST|UYT|UZT|VET|VLAT|VOLT|VOST|VUT|WAKT|WAST|WAT|WEDT|WEST|WET|WST|YAKT|YEKT)$/
|
10
|
-
|
11
25
|
ONE_DIGIT_REGEXP = /^\d{1}$/
|
12
26
|
TWO_DIGIT_REGEXP = /^\d{2}$/
|
13
27
|
FOUR_DIGIT_REGEXP = /^\d{4}$/
|
@@ -25,146 +39,115 @@ module Stamp
|
|
25
39
|
OBVIOUS_DAYS = 13..31
|
26
40
|
OBVIOUS_24_HOUR = 13..23
|
27
41
|
|
42
|
+
TWO_DIGIT_YEAR_EMITTER = Emitters::TwoDigit.new(:year) { |year| year % 100 }
|
43
|
+
TWO_DIGIT_MONTH_EMITTER = Emitters::TwoDigit.new(:month)
|
44
|
+
TWO_DIGIT_DAY_EMITTER = Emitters::TwoDigit.new(:day)
|
45
|
+
HOUR_TO_12_HOUR = lambda { |h| ((h - 1) % 12) + 1 }
|
46
|
+
|
28
47
|
OBVIOUS_DATE_MAP = {
|
29
|
-
OBVIOUS_YEARS =>
|
30
|
-
OBVIOUS_MONTHS =>
|
31
|
-
OBVIOUS_DAYS =>
|
48
|
+
OBVIOUS_YEARS => TWO_DIGIT_YEAR_EMITTER,
|
49
|
+
OBVIOUS_MONTHS => TWO_DIGIT_MONTH_EMITTER,
|
50
|
+
OBVIOUS_DAYS => TWO_DIGIT_DAY_EMITTER
|
32
51
|
}
|
33
52
|
|
34
53
|
TWO_DIGIT_DATE_SUCCESSION = {
|
35
|
-
:month =>
|
36
|
-
:day =>
|
37
|
-
:year =>
|
54
|
+
:month => TWO_DIGIT_DAY_EMITTER,
|
55
|
+
:day => TWO_DIGIT_YEAR_EMITTER,
|
56
|
+
:year => TWO_DIGIT_MONTH_EMITTER
|
38
57
|
}
|
39
58
|
|
40
59
|
TWO_DIGIT_TIME_SUCCESSION = {
|
41
|
-
:hour
|
42
|
-
:
|
60
|
+
:hour => Emitters::TwoDigit.new(:min),
|
61
|
+
:min => Emitters::TwoDigit.new(:sec)
|
43
62
|
}
|
44
63
|
|
45
|
-
def initialize(target_date_or_time)
|
46
|
-
@target = target_date_or_time
|
47
|
-
end
|
48
|
-
|
49
|
-
# Cribbed from ActiveSupport to format ordinal days (1st, 2nd, 23rd etc).
|
50
|
-
def ordinalize(number)
|
51
|
-
if (11..13).include?(number.to_i % 100)
|
52
|
-
"#{number}th"
|
53
|
-
else
|
54
|
-
case number.to_i % 10
|
55
|
-
when 1; "#{number}st"
|
56
|
-
when 2; "#{number}nd"
|
57
|
-
when 3; "#{number}rd"
|
58
|
-
else "#{number}th"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
64
|
def translate(example)
|
64
65
|
# extract any substrings that look like times, like "23:59" or "8:37 am"
|
65
66
|
before, time_example, after = example.partition(TIME_REGEXP)
|
66
67
|
|
67
|
-
#
|
68
|
-
|
69
|
-
|
68
|
+
# build emitters from the example date
|
69
|
+
emitters = Emitters::Composite.new
|
70
|
+
emitters << build_emitters(before.split(/\b/)) do |token, previous_part|
|
71
|
+
date_emitter(token, previous_part)
|
70
72
|
end
|
71
73
|
|
72
|
-
#
|
74
|
+
# build emitters from the example time
|
73
75
|
unless time_example.empty?
|
74
76
|
time_parts = time_example.scan(TIME_REGEXP).first
|
75
|
-
|
76
|
-
|
77
|
+
emitters << build_emitters(time_parts) do |token, previous_part|
|
78
|
+
time_emitter(token, previous_part)
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
80
82
|
# recursively process any remaining text
|
81
|
-
|
82
|
-
|
83
|
-
end
|
84
|
-
|
85
|
-
# Returns symbolic date part for given strftime directive.
|
86
|
-
def date_part(strftime_directive)
|
87
|
-
case strftime_directive
|
88
|
-
when '%b', '%B', '%m'
|
89
|
-
:month
|
90
|
-
when '%d', '%e'
|
91
|
-
:day
|
92
|
-
when '%y', '%Y'
|
93
|
-
:year
|
94
|
-
when '%H', '%I', '%l'
|
95
|
-
:hour
|
96
|
-
when '%M'
|
97
|
-
:minute
|
98
|
-
when '%S'
|
99
|
-
:second
|
100
|
-
end
|
83
|
+
emitters << translate(after) unless after.empty?
|
84
|
+
emitters
|
101
85
|
end
|
102
86
|
|
103
|
-
# Transforms tokens that look like date/time parts to
|
104
|
-
def
|
87
|
+
# Transforms tokens that look like date/time parts to emitter objects.
|
88
|
+
def build_emitters(tokens)
|
105
89
|
previous_part = nil
|
106
90
|
tokens.map do |token|
|
107
|
-
|
108
|
-
previous_part =
|
109
|
-
|
91
|
+
emitter = yield(token, previous_part)
|
92
|
+
previous_part = emitter.field unless emitter.nil?
|
93
|
+
|
94
|
+
emitter || Emitters::String.new(token)
|
110
95
|
end
|
111
96
|
end
|
112
97
|
|
113
|
-
def
|
98
|
+
def time_emitter(token, previous_part)
|
114
99
|
case token
|
115
100
|
when MERIDIAN_LOWER_REGEXP
|
116
|
-
|
117
|
-
# 1.8.7 doesn't implement %P
|
118
|
-
@target.strftime("%p").downcase
|
119
|
-
else
|
120
|
-
'%P'
|
121
|
-
end
|
101
|
+
Emitters::AmPm.new
|
122
102
|
|
123
103
|
when MERIDIAN_UPPER_REGEXP
|
124
|
-
|
104
|
+
Emitters::AmPm.new { |v| v.upcase }
|
125
105
|
|
126
106
|
when TWO_DIGIT_REGEXP
|
127
107
|
TWO_DIGIT_TIME_SUCCESSION[previous_part] ||
|
128
108
|
case token.to_i
|
129
109
|
when OBVIOUS_24_HOUR
|
130
|
-
|
110
|
+
# 24-hour clock
|
111
|
+
Emitters::TwoDigit.new(:hour)
|
131
112
|
else
|
132
|
-
|
113
|
+
# 12-hour clock with leading zero
|
114
|
+
Emitters::TwoDigit.new(:hour, &HOUR_TO_12_HOUR)
|
133
115
|
end
|
134
116
|
|
135
117
|
when ONE_DIGIT_REGEXP
|
136
|
-
|
118
|
+
# 12-hour clock without leading zero
|
119
|
+
Emitters::Delegate.new(:hour, &HOUR_TO_12_HOUR)
|
137
120
|
end
|
138
121
|
end
|
139
122
|
|
140
|
-
def
|
123
|
+
def date_emitter(token, previous_part)
|
141
124
|
case token
|
142
125
|
when MONTHNAMES_REGEXP
|
143
|
-
|
126
|
+
Emitters::Lookup.new(:month, Date::MONTHNAMES)
|
144
127
|
|
145
128
|
when ABBR_MONTHNAMES_REGEXP
|
146
|
-
|
129
|
+
Emitters::Lookup.new(:month, Date::ABBR_MONTHNAMES)
|
147
130
|
|
148
131
|
when DAYNAMES_REGEXP
|
149
|
-
|
132
|
+
Emitters::Lookup.new(:wday, Date::DAYNAMES)
|
150
133
|
|
151
134
|
when ABBR_DAYNAMES_REGEXP
|
152
|
-
|
135
|
+
Emitters::Lookup.new(:wday, Date::ABBR_DAYNAMES)
|
153
136
|
|
154
137
|
when TIMEZONE_REGEXP
|
155
|
-
|
138
|
+
Emitters::Delegate.new(:zone)
|
156
139
|
|
157
140
|
when FOUR_DIGIT_REGEXP
|
158
|
-
|
141
|
+
Emitters::Delegate.new(:year)
|
159
142
|
|
160
143
|
when ORDINAL_DAY_REGEXP
|
161
|
-
|
144
|
+
Emitters::Ordinal.new(:day)
|
162
145
|
|
163
146
|
when TWO_DIGIT_REGEXP
|
164
147
|
value = token.to_i
|
165
148
|
|
166
149
|
obvious_mappings =
|
167
|
-
OBVIOUS_DATE_MAP.reject { |k,v|
|
150
|
+
OBVIOUS_DATE_MAP.reject { |k,v| v.field == previous_part }
|
168
151
|
|
169
152
|
obvious_directive = obvious_mappings.find do |range, directive|
|
170
153
|
break directive if range === value
|
@@ -174,10 +157,10 @@ module Stamp
|
|
174
157
|
# disambiguate based on context
|
175
158
|
obvious_directive ||
|
176
159
|
TWO_DIGIT_DATE_SUCCESSION[previous_part] ||
|
177
|
-
|
160
|
+
TWO_DIGIT_MONTH_EMITTER
|
178
161
|
|
179
162
|
when ONE_DIGIT_REGEXP
|
180
|
-
|
163
|
+
Emitters::Delegate.new(:day)
|
181
164
|
end
|
182
165
|
end
|
183
166
|
|
data/lib/stamp/version.rb
CHANGED
data/stamp.gemspec
CHANGED
@@ -15,4 +15,7 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.test_files = `git ls-files -- features/*`.split("\n")
|
16
16
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
17
|
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency 'cucumber'
|
20
|
+
s.add_development_dependency 'rake'
|
18
21
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stamp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,40 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
13
|
-
dependencies:
|
12
|
+
date: 2012-12-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: cucumber
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
14
46
|
description: Format dates and times based on human-friendly examples, not arcane strftime
|
15
47
|
directives.
|
16
48
|
email:
|
@@ -22,8 +54,8 @@ files:
|
|
22
54
|
- .gitignore
|
23
55
|
- .travis.yml
|
24
56
|
- .yardopts
|
57
|
+
- CONTRIBUTING.md
|
25
58
|
- Gemfile
|
26
|
-
- Gemfile.lock
|
27
59
|
- LICENSE.txt
|
28
60
|
- README.md
|
29
61
|
- Rakefile
|
@@ -32,6 +64,14 @@ files:
|
|
32
64
|
- features/step_definitions/stamp_steps.rb
|
33
65
|
- features/support/env.rb
|
34
66
|
- lib/stamp.rb
|
67
|
+
- lib/stamp/emitters/am_pm.rb
|
68
|
+
- lib/stamp/emitters/composite.rb
|
69
|
+
- lib/stamp/emitters/delegate.rb
|
70
|
+
- lib/stamp/emitters/lookup.rb
|
71
|
+
- lib/stamp/emitters/modifiable.rb
|
72
|
+
- lib/stamp/emitters/ordinal.rb
|
73
|
+
- lib/stamp/emitters/string.rb
|
74
|
+
- lib/stamp/emitters/two_digit.rb
|
35
75
|
- lib/stamp/translator.rb
|
36
76
|
- lib/stamp/version.rb
|
37
77
|
- stamp.gemspec
|
@@ -49,7 +89,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
89
|
version: '0'
|
50
90
|
segments:
|
51
91
|
- 0
|
52
|
-
hash:
|
92
|
+
hash: 167698609462982419
|
53
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
94
|
none: false
|
55
95
|
requirements:
|
@@ -58,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
98
|
version: '0'
|
59
99
|
segments:
|
60
100
|
- 0
|
61
|
-
hash:
|
101
|
+
hash: 167698609462982419
|
62
102
|
requirements: []
|
63
103
|
rubyforge_project:
|
64
104
|
rubygems_version: 1.8.23
|
data/Gemfile.lock
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
stamp (0.4.0)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: http://rubygems.org/
|
8
|
-
specs:
|
9
|
-
builder (3.1.4)
|
10
|
-
cucumber (1.2.1)
|
11
|
-
builder (>= 2.1.2)
|
12
|
-
diff-lcs (>= 1.1.3)
|
13
|
-
gherkin (~> 2.11.0)
|
14
|
-
json (>= 1.4.6)
|
15
|
-
diff-lcs (1.1.3)
|
16
|
-
gherkin (2.11.5)
|
17
|
-
json (>= 1.4.6)
|
18
|
-
json (1.7.5)
|
19
|
-
rake (0.9.2.2)
|
20
|
-
|
21
|
-
PLATFORMS
|
22
|
-
ruby
|
23
|
-
|
24
|
-
DEPENDENCIES
|
25
|
-
cucumber
|
26
|
-
rake
|
27
|
-
stamp!
|