stamp 0.6.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5c0058768a5169006ec3da166fdb88f082715295
4
- data.tar.gz: fcf6e1e6fc53cdd3105b1eceb2099b4014b003fc
2
+ SHA256:
3
+ metadata.gz: f41c8f35a8e2cd599d4cff288999cadec0daecd942e963617a97c30fe2b6af16
4
+ data.tar.gz: 7f2da591b6b8c66fc6862b93bf93db1e86aac7212f4a043abfde0e6e0d3d4a1e
5
5
  SHA512:
6
- metadata.gz: 5be616540b39f2dac3e8bef2e886e809b908446f70e4d18d291e84dc17d22a376b12ac75332a844611be190bebc6df71a072247ed6d75ff8c8bd4e9da8b1ef6d
7
- data.tar.gz: c508e4cc1a1821006ced92ae88ca0b2bb7a766cf30d04eb26cdf56f244ed0f41788d08297993a89850bf625860cc38fec416e48448d36739c1d5611a931c2813
6
+ metadata.gz: 1cf409a95607824e5758705c2f152270e9294549bafcbb32e74f877575bed567310899170c9ae88c5c4ab411ac666c7f54bef133b67d3375fc5822e938b9b1e7
7
+ data.tar.gz: 9755ca6a5e9d1ff3c8a48e0adede18577699187985240580e77ba93c8912e5feb1ebf6197b31a3451394f1800e344ad3dac248fbdbcc5fec7a18be681d6004ae
@@ -0,0 +1,18 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby: ["3.1", "3.2", "3.3", "3.4"]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: ${{ matrix.ruby }}
17
+ bundler-cache: true
18
+ - run: bundle exec rake
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Jeremy Weiskotten
1
+ Copyright (c) 2011-2026 Jeremy Weiskotten
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Format dates and times based on human-friendly examples, not arcane
4
4
  [strftime](http://strfti.me) directives.
5
5
 
6
- [![Build Status](https://secure.travis-ci.org/jeremyw/stamp.png)](http://travis-ci.org/jeremyw/stamp)
6
+ [![CI](https://github.com/jeremyw/stamp/actions/workflows/ci.yml/badge.svg)](https://github.com/jeremyw/stamp/actions/workflows/ci.yml)
7
7
 
8
8
  ## Installation
9
9
 
@@ -58,6 +58,44 @@ 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
 
61
+ UTC offsets are recognized too, in both `+HHMM` and `+HH:MM` styles:
62
+
63
+ ```ruby
64
+ time.stamp("23:59 +00:00") #=> "20:52 -05:00"
65
+ ```
66
+
67
+ ### Without monkey patches
68
+
69
+ If you'd rather not patch `Date` and `Time`, use the module function:
70
+
71
+ ```ruby
72
+ require "stamp/core" # instead of "stamp"
73
+
74
+ Stamp.format(Date.new(2011, 6, 9), "Jan 1, 1999") #=> "Jun 9, 2011"
75
+ ```
76
+
77
+ Or a refinement, scoped to the files that opt in:
78
+
79
+ ```ruby
80
+ require "stamp/refinement"
81
+ using Stamp::Refinement
82
+
83
+ Date.new(2011, 6, 9).stamp("Jan 1, 1999") #=> "Jun 9, 2011"
84
+ ```
85
+
86
+ ### Converting examples to strftime formats
87
+
88
+ When you need the equivalent strftime string — for `Time::DATE_FORMATS`,
89
+ I18n configs, or APIs that want a format rather than a formatted value:
90
+
91
+ ```ruby
92
+ Stamp.strftime_format("Jan 1, 1999") #=> "%b %-d, %Y"
93
+ Stamp.strftime_format("23:59:59") #=> "%H:%M:%S"
94
+ ```
95
+
96
+ Ordinal days (e.g. "1st") have no strftime equivalent and raise an
97
+ `ArgumentError`.
98
+
61
99
  ## Features
62
100
 
63
101
  * Abbreviated and full names of months and weekdays are recognized.
@@ -102,5 +140,4 @@ Time.now.to_s(:military) #=> "16 July 15:35"
102
140
 
103
141
  ## Copyright
104
142
 
105
- Copyright (c) 2011 Jeremy Weiskotten (@doctorzaius). See LICENSE.txt for
106
- further details.
143
+ Copyright (c) 2011-2026 Jeremy Weiskotten. See LICENSE.txt for further details.
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
1
  require 'bundler/gem_tasks'
2
- require 'cucumber/rake/task'
2
+ require 'rake/testtask'
3
3
 
4
- Cucumber::Rake::Task.new(:features)
5
-
6
- Cucumber::Rake::Task.new('features:wip', 'Run Cucumber features that are a work in progress') do |t|
7
- t.profile = 'wip'
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.pattern = "test/**/*_test.rb"
8
8
  end
9
9
 
10
- task :default => :features
10
+ task :default => :test
data/lib/stamp/core.rb ADDED
@@ -0,0 +1,73 @@
1
+ require "date"
2
+ require "time"
3
+
4
+ require "stamp/emitters/am_pm"
5
+ require "stamp/emitters/ambiguous"
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/twelve_hour"
12
+ require "stamp/emitters/two_digit"
13
+ require "stamp/emitters/utc_offset"
14
+ require "stamp/disambiguator"
15
+ require "stamp/translator"
16
+ require "stamp/version"
17
+
18
+ module Stamp
19
+ # Formats a date/time using a human-friendly example as a template,
20
+ # without monkey patching Date or Time.
21
+ #
22
+ # @param [Date|Time|DateTime] target the date/time to format
23
+ # @param [String] example a human-friendly date/time example
24
+ #
25
+ # @return [String] the formatted date or time
26
+ #
27
+ # @example
28
+ # Stamp.format(Date.new(2012, 12, 21), "Jan 1, 1999") #=> "Dec 21, 2012"
29
+ def self.format(target, example)
30
+ emitters(example).format(target)
31
+ rescue NoMethodError => e
32
+ raise e if target.respond_to?(e.name)
33
+ raise ArgumentError,
34
+ "#{example.inspect} requires ##{e.name}, which #{target.class} doesn't implement (e.g. a Date can't format time-of-day examples)"
35
+ end
36
+
37
+ # Translates a human-friendly example into a strftime format string.
38
+ #
39
+ # @param [String] example a human-friendly date/time example
40
+ #
41
+ # @return [String] the equivalent strftime format
42
+ #
43
+ # @raise [ArgumentError] if the example uses a feature strftime can't
44
+ # express (e.g. ordinal days like "1st")
45
+ #
46
+ # @example
47
+ # Stamp.strftime_format("Jan 1, 1999") #=> "%b %-d, %Y"
48
+ def self.strftime_format(example)
49
+ emitters(example).strftime_format
50
+ end
51
+
52
+ # Memoizes the emitters for each example to improve performance.
53
+ def self.emitters(example)
54
+ cache = (@emitters ||= {})
55
+ cache.clear if cache.size >= 1000 # ponytail: crude cap to bound memory; LRU if it ever matters
56
+ cache[example] ||= Disambiguator.new(Translator.new.translate(example)).disambiguate!
57
+ end
58
+ private_class_method :emitters
59
+
60
+ # Formats a date/time using a human-friendly example as a template.
61
+ #
62
+ # @param [String] example a human-friendly date/time example
63
+ #
64
+ # @return [String] the formatted date or time
65
+ #
66
+ # @example
67
+ # Date.new(2012, 12, 21).stamp("Jan 1, 1999") #=> "Dec 21, 2012"
68
+ def stamp(example)
69
+ Stamp.format(self, example)
70
+ end
71
+ alias :stamp_like :stamp
72
+ alias :format_like :stamp
73
+ end
@@ -1,23 +1,25 @@
1
- class Disambiguator
2
- attr_reader :emitters
1
+ module Stamp
2
+ class Disambiguator
3
+ attr_reader :emitters
3
4
 
4
- def initialize(emitters)
5
- @emitters = emitters
6
- end
5
+ def initialize(emitters)
6
+ @emitters = emitters
7
+ end
7
8
 
8
- def disambiguate!
9
- emitters.replace_each! do |emitter|
10
- disambiguate(emitter)
9
+ def disambiguate!
10
+ emitters.replace_each! do |emitter|
11
+ disambiguate(emitter)
12
+ end
11
13
  end
12
- end
13
14
 
14
- private
15
+ private
15
16
 
16
- def disambiguate(emitter)
17
- if emitter.respond_to?(:disambiguate)
18
- emitter.disambiguate(emitters)
19
- else
20
- emitter
17
+ def disambiguate(emitter)
18
+ if emitter.respond_to?(:disambiguate)
19
+ emitter.disambiguate(emitters)
20
+ else
21
+ emitter
22
+ end
21
23
  end
22
24
  end
23
25
  end
@@ -1,22 +1,24 @@
1
1
  module Stamp
2
2
  module Emitters
3
3
  class AmPm
4
- include Modifiable
4
+ attr_reader :strftime_directive
5
5
 
6
- AM = 'am'
7
- PM = 'pm'
8
-
9
- def initialize(&block)
10
- @modifier = block
6
+ # @param [Array<String>]values for am pm text
7
+ def initialize(values, strftime_directive)
8
+ @values = values
9
+ @strftime_directive = strftime_directive
11
10
  end
12
11
 
13
12
  def format(target)
14
- modify(target.hour < 12 ? AM : PM)
13
+ @values[target.hour < 12 ? 0 : 1]
15
14
  end
16
15
 
17
16
  def field
18
17
  nil
19
18
  end
19
+
20
+ UPPERCASE = new(%w(AM PM), "%p")
21
+ LOWERCASE = new(%w(am pm), "%P")
20
22
  end
21
23
  end
22
- end
24
+ end
@@ -17,7 +17,17 @@ module Stamp
17
17
  result
18
18
  end
19
19
 
20
- def <<(emitter)
20
+ # Builds the equivalent strftime format string.
21
+ #
22
+ # @raise [ArgumentError] if any emitter has no strftime equivalent
23
+ def strftime_format
24
+ emitters.map do |e|
25
+ e.strftime_directive ||
26
+ raise(ArgumentError, "#{e.class.name.split('::').last} (e.g. \"1st\") has no strftime equivalent")
27
+ end.join
28
+ end
29
+
30
+ def <<(emitter)
21
31
  Array(emitter).each { |e| emitters << e }
22
32
  end
23
33
 
@@ -1,19 +1,23 @@
1
1
  module Stamp
2
2
  module Emitters
3
3
  class Delegate
4
- include Modifiable
4
+ attr_reader :field, :strftime_directive
5
5
 
6
- attr_reader :field
7
-
8
- # @param [field] the field to be formatted (e.g. +:month+, +:year+)
9
- def initialize(field, &block)
6
+ # @param [Symbol|String] field the field to be formatted (e.g. +:month+, +:year+)
7
+ def initialize(field, strftime_directive)
10
8
  @field = field
11
- @modifier = block
9
+ @strftime_directive = strftime_directive
12
10
  end
13
11
 
12
+ # @param [Date|Time|DateTime] target the date to be formatted
14
13
  def format(target)
15
- modify(target.send(field))
14
+ target.send(field)
16
15
  end
16
+
17
+ ZONE = new(:zone, "%Z")
18
+ YEAR = new(:year, "%Y")
19
+ MONTH = new(:month, "%-m")
20
+ DAY = new(:day, "%-d")
17
21
  end
18
22
  end
19
- end
23
+ end
@@ -1,14 +1,14 @@
1
1
  module Stamp
2
2
  module Emitters
3
3
  class Lookup
4
- attr_reader :field
4
+ attr_reader :field, :strftime_directive
5
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)
6
+ # @param [Symbol|String] field the field to be formatted (e.g. +:month+, +:year+)
7
+ # @param [Array<String>] lookup an array of the string values to be formatted (e.g. +Date::DAYNAMES+)
8
+ def initialize(field, lookup, strftime_directive)
10
9
  @field = field
11
10
  @lookup = lookup
11
+ @strftime_directive = strftime_directive
12
12
  end
13
13
 
14
14
  def format(target)
@@ -16,12 +16,13 @@ module Stamp
16
16
  end
17
17
 
18
18
  def lookup(value)
19
- if @lookup.respond_to?(:call)
20
- @lookup.call(value)
21
- else
22
- @lookup[value]
23
- end
19
+ @lookup[value]
24
20
  end
21
+
22
+ MONTH = new(:month, Date::MONTHNAMES, "%B")
23
+ ABBR_MONTH = new(:month, Date::ABBR_MONTHNAMES, "%b")
24
+ DAY = new(:wday, Date::DAYNAMES, "%A")
25
+ ABBR_DAY = new(:wday, Date::ABBR_DAYNAMES, "%a")
25
26
  end
26
27
  end
27
- end
28
+ end
@@ -3,11 +3,12 @@ module Stamp
3
3
  class Ordinal
4
4
  attr_reader :field
5
5
 
6
- # @param [field] the field to be formatted (e.g. +:month+, +:year+)
6
+ # @param [Symbol|String] field the field to be formatted (e.g. +:month+, +:year+)
7
7
  def initialize(field)
8
8
  @field = field
9
9
  end
10
10
 
11
+ # @param [Date|Time|DateTime] target the date to be formatted
11
12
  def format(target)
12
13
  ordinalize(target.send(field))
13
14
  end
@@ -26,6 +27,13 @@ module Stamp
26
27
  end
27
28
  end
28
29
  end
30
+
31
+ # strftime has no directive for ordinals ("1st", "22nd", ...)
32
+ def strftime_directive
33
+ nil
34
+ end
35
+
36
+ DAY = new(:day)
29
37
  end
30
38
  end
31
- end
39
+ end
@@ -15,6 +15,12 @@ module Stamp
15
15
  value << emitter.value
16
16
  end
17
17
 
18
+ # Literal text, with % escaped so strftime passes it through verbatim.
19
+ def strftime_directive
20
+ # value can be nil (unmatched optional capture groups become tokens)
21
+ value.to_s.gsub("%", "%%")
22
+ end
23
+
18
24
  def field
19
25
  nil
20
26
  end
@@ -0,0 +1,38 @@
1
+ module Stamp
2
+ module Emitters
3
+ # Emits hours as a two-digit number with a leading
4
+ # zero if necessary.
5
+ class TwelveHour
6
+ attr_reader :leading_zero
7
+
8
+ # @param [String|Symbol] field the field to be formatted (e.g. +:month+, +:year+)
9
+ # @param [Hash] options the options for output
10
+ # @option options [String] :leading_zero (default: false) pass true add a leading 0 to output
11
+ def initialize(options = {})
12
+ @leading_zero = options[:leading_zero]
13
+ end
14
+
15
+ # @param [Date|Time|DateTime] target the date to be formatted
16
+ def format(target)
17
+ value = target.hour
18
+ value = ((value - 1) % 12) + 1
19
+ if leading_zero && (value < 10)
20
+ "0#{value}"
21
+ else
22
+ value
23
+ end
24
+ end
25
+
26
+ def field
27
+ :hour
28
+ end
29
+
30
+ def strftime_directive
31
+ leading_zero ? "%I" : "%-I"
32
+ end
33
+
34
+ LEADING_ZERO = new(leading_zero: true)
35
+ NON_LEADING_ZERO = new(leading_zero: false)
36
+ end
37
+ end
38
+ end
@@ -3,20 +3,29 @@ module Stamp
3
3
  # Emits the given field as a two-digit number with a leading
4
4
  # zero if necessary.
5
5
  class TwoDigit
6
- include Modifiable
6
+ attr_reader :field, :strftime_directive
7
7
 
8
- attr_reader :field
9
-
10
- # @param [field] the field to be formatted (e.g. +:month+, +:year+)
11
- def initialize(field, &block)
8
+ # @param [String|Symbol] field the field to be formatted (e.g. +:month+, +:year+)
9
+ def initialize(field, strftime_directive)
12
10
  @field = field
13
- @modifier = block
11
+ @strftime_directive = strftime_directive
14
12
  end
15
13
 
16
14
  def format(target)
17
- value = modify(target.send(field))
18
- value < 10 ? "0#{value}" : value
15
+ value = target.send(field) % 100
16
+ # by definition, leading_zero option is true
17
+ if value < 10
18
+ "0#{value}"
19
+ else
20
+ value
21
+ end
19
22
  end
23
+ YEAR = new(:year, "%y")
24
+ MONTH = new(:month, "%m")
25
+ DAY = new(:day, "%d")
26
+ HOUR = new(:hour, "%H")
27
+ MIN = new(:min, "%M")
28
+ SEC = new(:sec, "%S")
20
29
  end
21
30
  end
22
- end
31
+ end
@@ -0,0 +1,27 @@
1
+ module Stamp
2
+ module Emitters
3
+ # Emits the target's UTC offset, e.g. "-0500" or "-05:00".
4
+ class UtcOffset
5
+ # @param [String] directive the strftime directive to delegate to
6
+ # (+"%z"+ or +"%:z"+)
7
+ def initialize(directive)
8
+ @directive = directive
9
+ end
10
+
11
+ def format(target)
12
+ target.strftime(@directive)
13
+ end
14
+
15
+ def strftime_directive
16
+ @directive
17
+ end
18
+
19
+ def field
20
+ nil
21
+ end
22
+
23
+ PLAIN = new("%z")
24
+ COLON = new("%:z")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ require "stamp/core"
2
+
3
+ module Stamp
4
+ # An alternative to the default Date/Time monkey patches:
5
+ #
6
+ # require "stamp/refinement"
7
+ # using Stamp::Refinement
8
+ #
9
+ # Date.today.stamp("Jan 1, 1999")
10
+ module Refinement
11
+ [Date, Time].each do |klass|
12
+ refine klass do
13
+ def stamp(example)
14
+ Stamp.format(self, example)
15
+ end
16
+ alias_method :stamp_like, :stamp
17
+ alias_method :format_like, :stamp
18
+ end
19
+ end
20
+ end
21
+ end
@@ -5,7 +5,7 @@ module Stamp
5
5
  # http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
6
6
  TIME_ZONE_ABBREVIATIONS = %w{
7
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
8
+ BDT BIOT BIT BOT BRT BST BTT CAT CCT CDT CEDT CEST CET CHADT CHAST CHOT ChST CHUT
9
9
  CIST CIT CKT CLST CLT COST COT CST CT CVT CWST CXT DAVT DDUT DFT EASST EAST EAT
10
10
  ECT EDT EEDT EEST EET EGST EGT EIT EST FET FJT FKST FKT FNT GALT GAMT GET GFT
11
11
  GILT GIT GMT GST GYT HADT HAEC HAST HKT HMT HOVT HST ICT IDT IOT IRDT IRKT IRST
@@ -28,6 +28,10 @@ module Stamp
28
28
 
29
29
  TIME_REGEXP = /(\d{1,2})(:)(\d{2})(\s*)(:)?(\d{2})?(\s*)?([ap]m)?/i
30
30
 
31
+ # UTC offsets like "+05:00" or "-0500". The lookbehind/lookahead keep
32
+ # date fragments like "12-31-1999" from matching; hours are capped at 14.
33
+ UTC_OFFSET_REGEXP = /(?<![-\d])[+-](?:0\d|1[0-4]):?[0-5]\d(?!\d)/
34
+
31
35
  MERIDIAN_LOWER_REGEXP = /^(a|p)m$/
32
36
  MERIDIAN_UPPER_REGEXP = /^(A|P)M$/
33
37
 
@@ -38,12 +42,18 @@ module Stamp
38
42
  OBVIOUS_DAY = 13..31
39
43
  OBVIOUS_YEAR = 32..99
40
44
 
41
- TWO_DIGIT_YEAR_EMITTER = Emitters::TwoDigit.new(:year) { |year| year % 100 }
42
- TWO_DIGIT_MONTH_EMITTER = Emitters::TwoDigit.new(:month)
43
- TWO_DIGIT_DAY_EMITTER = Emitters::TwoDigit.new(:day)
44
- HOUR_TO_12_HOUR = lambda { |h| ((h - 1) % 12) + 1 }
45
-
46
45
  def translate(example)
46
+ # extract any substrings that look like UTC offsets, like "+05:00" or
47
+ # "-0500", before the time logic can mistake them for clock times
48
+ before, offset_example, after = example.partition(UTC_OFFSET_REGEXP)
49
+ unless offset_example.empty?
50
+ emitters = Emitters::Composite.new
51
+ emitters << translate(before) unless before.empty?
52
+ emitters << (offset_example.include?(":") ? Emitters::UtcOffset::COLON : Emitters::UtcOffset::PLAIN)
53
+ emitters << translate(after) unless after.empty?
54
+ return emitters
55
+ end
56
+
47
57
  # extract any substrings that look like times, like "23:59" or "8:37 am"
48
58
  before, time_example, after = example.partition(TIME_REGEXP)
49
59
 
@@ -76,20 +86,20 @@ module Stamp
76
86
  def time_emitter(token)
77
87
  case token
78
88
  when MERIDIAN_LOWER_REGEXP
79
- Emitters::AmPm.new
89
+ Emitters::AmPm::LOWERCASE
80
90
 
81
91
  when MERIDIAN_UPPER_REGEXP
82
- Emitters::AmPm.new { |v| v.upcase }
92
+ Emitters::AmPm::UPPERCASE
83
93
 
84
94
  when TWO_DIGIT_REGEXP
85
95
  Emitters::Ambiguous.new(
86
96
  two_digit_hour_emitter(token),
87
- Emitters::TwoDigit.new(:min),
88
- Emitters::TwoDigit.new(:sec))
97
+ Emitters::TwoDigit::MIN,
98
+ Emitters::TwoDigit::SEC)
89
99
 
90
100
  when ONE_DIGIT_REGEXP
91
101
  # 12-hour clock without leading zero
92
- Emitters::Delegate.new(:hour, &HOUR_TO_12_HOUR)
102
+ Emitters::TwelveHour::NON_LEADING_ZERO
93
103
  end
94
104
  end
95
105
 
@@ -97,57 +107,54 @@ module Stamp
97
107
  case token.to_i
98
108
  when OBVIOUS_24_HOUR
99
109
  # 24-hour clock
100
- Emitters::TwoDigit.new(:hour)
110
+ Emitters::TwoDigit::HOUR
101
111
  else
102
112
  # 12-hour clock with leading zero
103
- Emitters::TwoDigit.new(:hour, &HOUR_TO_12_HOUR)
113
+ Emitters::TwelveHour::LEADING_ZERO
104
114
  end
105
115
  end
106
116
 
107
117
  def date_emitter(token)
108
118
  case token
109
119
  when MONTHNAMES_REGEXP
110
- Emitters::Lookup.new(:month, Date::MONTHNAMES)
120
+ Emitters::Lookup::MONTH
111
121
 
112
122
  when ABBR_MONTHNAMES_REGEXP
113
- Emitters::Lookup.new(:month, Date::ABBR_MONTHNAMES)
123
+ Emitters::Lookup::ABBR_MONTH
114
124
 
115
125
  when DAYNAMES_REGEXP
116
- Emitters::Lookup.new(:wday, Date::DAYNAMES)
126
+ Emitters::Lookup::DAY
117
127
 
118
128
  when ABBR_DAYNAMES_REGEXP
119
- Emitters::Lookup.new(:wday, Date::ABBR_DAYNAMES)
129
+ Emitters::Lookup::ABBR_DAY
120
130
 
121
131
  when TIMEZONE_REGEXP
122
- Emitters::Delegate.new(:zone)
132
+ Emitters::Delegate::ZONE
123
133
 
124
134
  when FOUR_DIGIT_REGEXP
125
- Emitters::Delegate.new(:year)
135
+ Emitters::Delegate::YEAR
126
136
 
127
137
  when ORDINAL_DAY_REGEXP
128
- Emitters::Ordinal.new(:day)
138
+ Emitters::Ordinal::DAY
129
139
 
130
140
  when TWO_DIGIT_REGEXP
131
- value = token.to_i
132
-
133
- case value
141
+ case token.to_i
134
142
  when OBVIOUS_DAY
135
- TWO_DIGIT_DAY_EMITTER
143
+ Emitters::TwoDigit::DAY
136
144
  when OBVIOUS_YEAR
137
- TWO_DIGIT_YEAR_EMITTER
145
+ Emitters::TwoDigit::YEAR
138
146
  else
139
147
  Emitters::Ambiguous.new(
140
- TWO_DIGIT_MONTH_EMITTER,
141
- TWO_DIGIT_DAY_EMITTER,
142
- TWO_DIGIT_YEAR_EMITTER)
148
+ Emitters::TwoDigit::MONTH,
149
+ Emitters::TwoDigit::DAY,
150
+ Emitters::TwoDigit::YEAR)
143
151
  end
144
152
 
145
153
  when ONE_DIGIT_REGEXP
146
154
  Emitters::Ambiguous.new(
147
- Emitters::Delegate.new(:month),
148
- Emitters::Delegate.new(:day))
155
+ Emitters::Delegate::MONTH,
156
+ Emitters::Delegate::DAY)
149
157
  end
150
158
  end
151
-
152
159
  end
153
160
  end
data/lib/stamp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Stamp
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/stamp.rb CHANGED
@@ -1,50 +1,4 @@
1
- require "date"
2
- require "time"
1
+ require "stamp/core"
3
2
 
4
- require "stamp/emitters/modifiable"
5
- require "stamp/emitters/am_pm"
6
- require "stamp/emitters/ambiguous"
7
- require "stamp/emitters/composite"
8
- require "stamp/emitters/delegate"
9
- require "stamp/emitters/lookup"
10
- require "stamp/emitters/ordinal"
11
- require "stamp/emitters/string"
12
- require "stamp/emitters/two_digit"
13
- require "stamp/disambiguator"
14
- require "stamp/translator"
15
- require "stamp/version"
16
-
17
- module Stamp
18
- # Formats a date/time using a human-friendly example as a template.
19
- #
20
- # @param [String] example a human-friendly date/time example
21
- # @param [Hash] options
22
- # @option options [Boolean] :memoize (true)
23
- #
24
- # @return [String] the formatted date or time
25
- #
26
- # @example
27
- # Date.new(2012, 12, 21).stamp("Jan 1, 1999") #=> "Dec 21, 2012"
28
- def stamp(example)
29
- memoize_stamp_emitters(example).format(self)
30
- end
31
- alias :stamp_like :stamp
32
- alias :format_like :stamp
33
-
34
- private
35
-
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[example] ||= stamp_emitters(example)
41
- end
42
-
43
- def stamp_emitters(example)
44
- emitters = Translator.new.translate(example)
45
- Disambiguator.new(emitters).disambiguate!
46
- end
47
- end
48
-
49
- Date.send(:include, ::Stamp)
50
- Time.send(:include, ::Stamp)
3
+ Date.include(::Stamp)
4
+ Time.include(::Stamp)
data/stamp.gemspec CHANGED
@@ -10,12 +10,18 @@ Gem::Specification.new do |s|
10
10
  s.homepage = "https://github.com/jeremyw/stamp"
11
11
  s.summary = %Q{Date and time formatting for humans.}
12
12
  s.description = %Q{Format dates and times based on human-friendly examples, not arcane strftime directives.}
13
+ s.license = "MIT"
13
14
 
14
- s.files = `git ls-files`.split("\n")
15
- s.test_files = `git ls-files -- features/*`.split("\n")
16
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15
+ s.metadata = {
16
+ "source_code_uri" => "https://github.com/jeremyw/stamp",
17
+ "bug_tracker_uri" => "https://github.com/jeremyw/stamp/issues"
18
+ }
19
+
20
+ s.required_ruby_version = ">= 3.1"
21
+
22
+ s.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
23
  s.require_paths = ["lib"]
18
24
 
19
- s.add_development_dependency 'cucumber'
25
+ s.add_development_dependency 'minitest'
20
26
  s.add_development_dependency 'rake'
21
27
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Weiskotten
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-21 00:00:00.000000000 Z
11
+ date: 2026-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: cucumber
14
+ name: minitest
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -46,35 +46,37 @@ executables: []
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
+ - ".github/workflows/ci.yml"
49
50
  - ".gitignore"
50
- - ".travis.yml"
51
51
  - ".yardopts"
52
52
  - CONTRIBUTING.md
53
53
  - Gemfile
54
54
  - LICENSE.txt
55
55
  - README.md
56
56
  - Rakefile
57
- - cucumber.yml
58
- - features/stamp.feature
59
- - features/step_definitions/stamp_steps.rb
60
- - features/support/env.rb
61
57
  - lib/stamp.rb
58
+ - lib/stamp/core.rb
62
59
  - lib/stamp/disambiguator.rb
63
60
  - lib/stamp/emitters/am_pm.rb
64
61
  - lib/stamp/emitters/ambiguous.rb
65
62
  - lib/stamp/emitters/composite.rb
66
63
  - lib/stamp/emitters/delegate.rb
67
64
  - lib/stamp/emitters/lookup.rb
68
- - lib/stamp/emitters/modifiable.rb
69
65
  - lib/stamp/emitters/ordinal.rb
70
66
  - lib/stamp/emitters/string.rb
67
+ - lib/stamp/emitters/twelve_hour.rb
71
68
  - lib/stamp/emitters/two_digit.rb
69
+ - lib/stamp/emitters/utc_offset.rb
70
+ - lib/stamp/refinement.rb
72
71
  - lib/stamp/translator.rb
73
72
  - lib/stamp/version.rb
74
73
  - stamp.gemspec
75
74
  homepage: https://github.com/jeremyw/stamp
76
- licenses: []
77
- metadata: {}
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ source_code_uri: https://github.com/jeremyw/stamp
79
+ bug_tracker_uri: https://github.com/jeremyw/stamp/issues
78
80
  post_install_message:
79
81
  rdoc_options: []
80
82
  require_paths:
@@ -83,19 +85,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
85
  requirements:
84
86
  - - ">="
85
87
  - !ruby/object:Gem::Version
86
- version: '0'
88
+ version: '3.1'
87
89
  required_rubygems_version: !ruby/object:Gem::Requirement
88
90
  requirements:
89
91
  - - ">="
90
92
  - !ruby/object:Gem::Version
91
93
  version: '0'
92
94
  requirements: []
93
- rubyforge_project:
94
- rubygems_version: 2.2.2
95
+ rubygems_version: 3.0.3.1
95
96
  signing_key:
96
97
  specification_version: 4
97
98
  summary: Date and time formatting for humans.
98
- test_files:
99
- - features/stamp.feature
100
- - features/step_definitions/stamp_steps.rb
101
- - features/support/env.rb
99
+ test_files: []
data/.travis.yml DELETED
@@ -1,9 +0,0 @@
1
- rvm:
2
- - 1.8.7
3
- - 1.9.2
4
- - 1.9.3
5
- - 2.0.0
6
- - 2.1.2
7
- - ree
8
- - rbx
9
- - jruby
data/cucumber.yml DELETED
@@ -1,3 +0,0 @@
1
- ---
2
- default: --tags ~@wip --strict
3
- wip: --tags @wip --wip
@@ -1,123 +0,0 @@
1
- @stamp
2
- Feature: Stamping a date
3
- In order to format dates in a more programmer-friendly way
4
- the stamp method
5
- formats a date given a human-readable example.
6
-
7
- @date
8
- Scenario Outline: Formatting dates by example
9
- Given the date September 8, 2011
10
- When I stamp the example "<example>"
11
- Then I produce "<output>"
12
-
13
- Examples:
14
- | example | output |
15
- | January | September |
16
- | Jan | Sep |
17
- | Jan 1 | Sep 8 |
18
- | Jan 01 | Sep 08 |
19
- | Jan 10 | Sep 08 |
20
- | Jan 1, 1999 | Sep 8, 2011 |
21
- | Jan 12, 1999 | Sep 08, 2011 |
22
- | 1 Jan 1999 | 8 Sep 2011 |
23
- | 1/1/1999 | 9/8/2011 |
24
- | 31/01/2013 | 08/09/2011 |
25
- | 13 January 1999 | 08 September 2011 |
26
- | Monday | Thursday |
27
- | Tue, Jan 1 | Thu, Sep 8 |
28
- | Tuesday, January 1, 1999 | Thursday, September 8, 2011 |
29
- | 01/1999 | 09/2011 |
30
- | 01/01 | 09/08 |
31
- | 01/31 | 09/08 |
32
- | 01/99 | 09/11 |
33
- | 01/01/1999 | 09/08/2011 |
34
- | 12/31/99 | 09/08/11 |
35
- | 31/12 | 08/09 |
36
- | 31/12/99 | 08/09/11 |
37
- | 31-Jan-1999 | 08-Sep-2011 |
38
- | 1999-01-01 | 2011-09-08 |
39
- | 1999-12-31 | 2011-09-08 |
40
- | DOB: 12-31-1999 | DOB: 09-08-2011 |
41
-
42
- @date
43
- Scenario Outline: Formatting dates with ordinal days
44
- Given the date <date>
45
- When I stamp the example "<example>"
46
- Then I produce "<output>"
47
-
48
- Examples:
49
- | date | example | output |
50
- | Jan 1, 1999 | July 4th | January 1st |
51
- | Jan 2, 1999 | Dec 3rd | Jan 2nd |
52
- | Jan 3, 1999 | Dec 2nd | Jan 3rd |
53
- | Jan 4, 1999 | Jul 1st | Jan 4th |
54
- | Jan 5, 1999 | Dec 1st | Jan 5th |
55
- | Jan 6, 1999 | Dec 1st | Jan 6th |
56
- | Jan 7, 1999 | Dec 1st | Jan 7th |
57
- | Jan 8, 1999 | Dec 1st | Jan 8th |
58
- | Jan 9, 1999 | Dec 1st | Jan 9th |
59
- | Jan 10, 1999 | Dec 1st | Jan 10th |
60
- | Jan 11, 1999 | Dec 1st | Jan 11th |
61
- | Jan 12, 1999 | Dec 1st | Jan 12th |
62
- | Jan 13, 1999 | Dec 1st | Jan 13th |
63
- | Jan 14, 1999 | Dec 1st | Jan 14th |
64
- | Jan 20, 1999 | Dec 1st | Jan 20th |
65
- | Jan 21, 1999 | Dec 1st | Jan 21st |
66
- | Jan 22, 1999 | Dec 1st | Jan 22nd |
67
- | Jan 23, 1999 | Dec 1st | Jan 23rd |
68
- | Jan 24, 1999 | Dec 1st | Jan 24th |
69
- | Jan 1, 1999 | 4th of July | 1st of January |
70
- | Jan 1, 1999 | 4th of July, 1999 | 1st of January, 1999 |
71
-
72
- @time
73
- Scenario Outline: Formatting times by example
74
- Given the time zone is "EST"
75
- And the time February 8, 2011 at 13:31:27
76
- When I stamp the example "<example>"
77
- Then I produce "<output>"
78
-
79
- Examples:
80
- | example | output |
81
- | 8:59 am | 1:31 pm |
82
- | 8:59am | 1:31pm |
83
- | 08:59 AM | 01:31 PM |
84
- | 08:59 PM | 01:31 PM |
85
- | 23:59 | 13:31 |
86
- | 8:59:59 am | 1:31:27 pm |
87
- | 08:59:59 AM | 01:31:27 PM |
88
- | 08:59:59 PM | 01:31:27 PM |
89
- | 23:59:59 | 13:31:27 |
90
- | 8:59 PST | 1:31 EST |
91
-
92
- @date
93
- @time
94
- Scenario Outline: Formatting dates and times by example
95
- Given the time September 8, 2011 at 13:31:27
96
- When I stamp the example "<example>"
97
- Then I produce "<output>"
98
-
99
- Examples:
100
- | example | output |
101
- | Jan 1, 1999 8:59 am | Sep 8, 2011 1:31 pm |
102
- | 08:59 AM 1999-12-31 | 01:31 PM 2011-09-08 |
103
- | Date: Jan 1, 1999 Time: 8:59 am | Date: Sep 8, 2011 Time: 1:31 pm |
104
-
105
- Scenario: strftime directives just get passed through
106
- Given the date December 21, 2012
107
- When I stamp the example "John Cusack was in a movie about Jan (%-m) %e, %Y, but it wasn't very good."
108
- Then I produce "John Cusack was in a movie about Dec (%-m) %e, %Y, but it wasn't very good."
109
-
110
- Scenario: Plain text just gets passed through
111
- Given the date June 1, 1926
112
- When I stamp the example "Marilyn Monroe was born on January 9, 1999."
113
- Then I produce "Marilyn Monroe was born on June 1, 1926."
114
-
115
- Scenario Outline: Aliases for the stamp method
116
- Given the date December 9, 2011
117
- When I call "<alias>" with "1999-01-31"
118
- Then I produce "2011-12-09"
119
-
120
- Examples:
121
- | alias |
122
- | stamp_like |
123
- | format_like |
@@ -1,30 +0,0 @@
1
- module StampStepHelpers
2
- def month(month_name)
3
- Date::MONTHNAMES.index(month_name) || Date::ABBR_MONTHNAMES.index(month_name)
4
- end
5
- end
6
- World(StampStepHelpers)
7
-
8
- Given /^the date (\w+) (\d+), (\d{4})$/ do |month_name, day, year|
9
- @target = Date.new(year.to_i, month(month_name), day.to_i)
10
- end
11
-
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.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
18
- end
19
-
20
- When /^I stamp the example "([^"]*)"$/ do |example|
21
- @stamped = @target.stamp(example)
22
- end
23
-
24
- Then /^I produce "([^"]*)"$/ do |expected|
25
- assert_equal expected.strip, @stamped.strip
26
- end
27
-
28
- When /^I call "([^"]*)" with "([^"]*)"$/ do |method, arg|
29
- @stamped = @target.send(method, arg)
30
- end
@@ -1,14 +0,0 @@
1
- require 'bundler'
2
- begin
3
- Bundler.setup(:default, :development)
4
- rescue Bundler::BundlerError => e
5
- $stderr.puts e.message
6
- $stderr.puts "Run `bundle install` to install missing gems"
7
- exit e.status_code
8
- end
9
-
10
- $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
- require 'stamp'
12
-
13
- require 'test/unit/assertions'
14
- World(Test::Unit::Assertions)
@@ -1,9 +0,0 @@
1
- module Modifiable
2
- def modify(value)
3
- if @modifier
4
- @modifier.call(value)
5
- else
6
- value
7
- end
8
- end
9
- end