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 CHANGED
@@ -3,3 +3,4 @@
3
3
  .yardoc
4
4
  doc
5
5
  pkg/*
6
+ /Gemfile.lock
@@ -2,6 +2,7 @@ rvm:
2
2
  - 1.8.7
3
3
  - 1.9.2
4
4
  - 1.9.3
5
+ - 2.0.0
5
6
  - ree
6
7
  - rbx
7
- - jruby
8
+ - jruby
@@ -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
@@ -1,8 +1,3 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  gemspec
4
-
5
- group :development do
6
- gem 'cucumber'
7
- gem 'rake'
8
- end
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 available:
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 9, 2011"
31
- date.stamp("Jan 1, 1999") #=> "Jun 9, 2011"
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 9, 2011"
34
- date.stamp("Sun Aug 5") #=> "Thu Jun 9"
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") #=> " 8:52 PM"
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 9 at 08:52 PM"
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 the `strftime_format` method:
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/time_formats.rb
88
- Date::DATE_FORMATS[:short] = Stamp.strftime_format("Mon Jan 1")
89
- Time::DATE_FORMATS[:military] = Stamp.strftime_format("23:59")
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, but it hasn't been thoroughly tested. Patches welcome!
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
 
@@ -14,15 +14,15 @@ Feature: Stamping a date
14
14
  | example | output |
15
15
  | January | September |
16
16
  | Jan | Sep |
17
- | Jan 1 | Sep 8 |
17
+ | Jan 1 | Sep 8 |
18
18
  | Jan 01 | Sep 08 |
19
19
  | Jan 10 | Sep 08 |
20
- | Jan 1, 1999 | Sep 8, 2011 |
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 8 |
25
- | Tuesday, January 1, 1999 | Thursday, September 8, 2011 |
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 September 8, 2011 at 13:31:27
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 UTC |
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 8, 2011 1:31 pm |
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 8, 2011 Time: 1:31 pm |
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 %b %d, %Y, but it wasn't very good."
103
- Then I produce "John Cusack was in a movie about Dec 21, 2012, but it wasn't very good."
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 1, 1999."
108
- Then I produce "Marilyn Monroe was born on June 1, 1926."
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.utc(year.to_i, month(month_name), day.to_i, hours.to_i, minutes.to_i, seconds.to_i)
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|
@@ -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
- # Transforms the given example dates/time format to a format string
10
- # suitable for strftime.
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 [String] example a human-friendly date/time example
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
- strftime(strftime_format(example))
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
- # Transforms the given example date/time format to a format string
37
- # suitable for strftime.
38
- #
39
- # @param [String] example a human-friendly date/time example
40
- # @return [String] a strftime-friendly format
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,9 @@
1
+ module Modifiable
2
+ def modify(value)
3
+ if @modifier
4
+ @modifier.call(value)
5
+ else
6
+ value
7
+ end
8
+ end
9
+ 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
@@ -1,13 +1,27 @@
1
1
  module Stamp
2
- class StrftimeTranslator
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 => '%y',
30
- OBVIOUS_MONTHS => '%m',
31
- OBVIOUS_DAYS => '%d'
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 => '%d',
36
- :day => '%y',
37
- :year => '%m'
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 => '%M',
42
- :minute => '%S'
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
- # transform any date tokens to strftime directives
68
- words = strftime_directives(before.split(/\b/)) do |token, previous_part|
69
- strftime_date_directive(token, previous_part)
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
- # transform the example time string to strftime directives
74
+ # build emitters from the example time
73
75
  unless time_example.empty?
74
76
  time_parts = time_example.scan(TIME_REGEXP).first
75
- words += strftime_directives(time_parts) do |token, previous_part|
76
- strftime_time_directive(token, previous_part)
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
- words << translate(after) unless after.empty?
82
- words.join
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 strftime directives.
104
- def strftime_directives(tokens)
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
- directive = yield(token, previous_part)
108
- previous_part = date_part(directive) unless directive.nil?
109
- directive || token
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 strftime_time_directive(token, previous_part)
98
+ def time_emitter(token, previous_part)
114
99
  case token
115
100
  when MERIDIAN_LOWER_REGEXP
116
- if RUBY_VERSION =~ /^1.8/ && @target.is_a?(Time)
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
- '%p'
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
- '%H' # 24-hour clock
110
+ # 24-hour clock
111
+ Emitters::TwoDigit.new(:hour)
131
112
  else
132
- '%I' # 12-hour clock with leading zero
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
- '%l' # hour without leading zero
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 strftime_date_directive(token, previous_part)
123
+ def date_emitter(token, previous_part)
141
124
  case token
142
125
  when MONTHNAMES_REGEXP
143
- '%B'
126
+ Emitters::Lookup.new(:month, Date::MONTHNAMES)
144
127
 
145
128
  when ABBR_MONTHNAMES_REGEXP
146
- '%b'
129
+ Emitters::Lookup.new(:month, Date::ABBR_MONTHNAMES)
147
130
 
148
131
  when DAYNAMES_REGEXP
149
- '%A'
132
+ Emitters::Lookup.new(:wday, Date::DAYNAMES)
150
133
 
151
134
  when ABBR_DAYNAMES_REGEXP
152
- '%a'
135
+ Emitters::Lookup.new(:wday, Date::ABBR_DAYNAMES)
153
136
 
154
137
  when TIMEZONE_REGEXP
155
- '%Z'
138
+ Emitters::Delegate.new(:zone)
156
139
 
157
140
  when FOUR_DIGIT_REGEXP
158
- '%Y'
141
+ Emitters::Delegate.new(:year)
159
142
 
160
143
  when ORDINAL_DAY_REGEXP
161
- ordinalize(@target.day)
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| date_part(v) == previous_part }
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
- '%m'
160
+ TWO_DIGIT_MONTH_EMITTER
178
161
 
179
162
  when ONE_DIGIT_REGEXP
180
- '%e' # day without leading zero
163
+ Emitters::Delegate.new(:day)
181
164
  end
182
165
  end
183
166
 
@@ -1,3 +1,3 @@
1
1
  module Stamp
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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.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-06 00:00:00.000000000 Z
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: 2023252387998863659
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: 2023252387998863659
101
+ hash: 167698609462982419
62
102
  requirements: []
63
103
  rubyforge_project:
64
104
  rubygems_version: 1.8.23
@@ -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!