midnight 0.0.1.pre

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.
@@ -0,0 +1,153 @@
1
+ class Midnight::Repeater < Chronic::Tag #:nodoc:
2
+ #
3
+ def self.scan(tokens)
4
+ # for each token
5
+ tokens.each do |token|
6
+ token = self.scan_for_24h_times(token)
7
+ token = self.scan_for_numbers(token)
8
+ token = self.scan_for_month_names(token)
9
+ token = self.scan_for_day_names(token)
10
+ token = self.scan_for_special_text(token)
11
+ token = self.scan_for_units(token)
12
+ end
13
+
14
+ tokens = self.split_time_tokens(tokens)
15
+ tokens
16
+ end
17
+
18
+ def self.scan_for_numbers(token)
19
+ num = Float(token.word) rescue nil
20
+ token.update(:number, nil, num.to_i) if num
21
+ token
22
+ end
23
+
24
+ def self.scan_for_24h_times(token)
25
+ if token.word =~ /(2[0-3]|[01]?[0-9]):([0-5]?[0-9])/
26
+ token.update(:time, nil, nil)
27
+ end
28
+ token
29
+ end
30
+
31
+ def self.scan_for_month_names(token)
32
+ scanner = {
33
+ /^jan\.?(uary)?$/ => :january,
34
+ /^feb\.?(ruary)?$/ => :february,
35
+ /^mar\.?(ch)?$/ => :march,
36
+ /^apr\.?(il)?$/ => :april,
37
+ /^may$/ => :may,
38
+ /^jun\.?e?$/ => :june,
39
+ /^jul\.?y?$/ => :july,
40
+ /^aug\.?(ust)?$/ => :august,
41
+ /^sep\.?(t\.?|tember)?$/ => :september,
42
+ /^oct\.?(ober)?$/ => :october,
43
+ /^nov\.?(ember)?$/ => :november,
44
+ /^dec\.?(ember)?$/ => :december}
45
+
46
+ month_sequence = {
47
+ :january => 1,
48
+ :february => 2,
49
+ :march => 3,
50
+ :april => 4,
51
+ :may => 5,
52
+ :june => 6,
53
+ :july => 7,
54
+ :august => 8,
55
+ :september => 9,
56
+ :october => 10,
57
+ :november => 11,
58
+ :december => 12
59
+ }
60
+
61
+ scanner.each do |scanner_item, month|
62
+ position_in_sequence = month_sequence[month]
63
+ token.update(:month_name, scanner[scanner_item], 30, position_in_sequence) if scanner_item =~ token.word
64
+ end
65
+ token
66
+ end
67
+
68
+ def self.scan_for_day_names(token)
69
+ scanner = {
70
+ /^su[nm](day)?s?$/ => :sunday,
71
+ /^m[ou]n(day)?s?$/ => :monday,
72
+ /^t(ue|eu|oo|u|)s(day)?s?$/ => :tuesday,
73
+ /^tue$/ => :tuesday,
74
+ /^we(dnes|nds|nns)day?s?$/ => :wednesday,
75
+ /^wed$/ => :wednesday,
76
+ /^th(urs|ers)day?s?$/ => :thursday,
77
+ /^thu$/ => :thursday,
78
+ /^fr[iy](day)?s?$/ => :friday,
79
+ /^sat(t?[ue]rday)?s?$/ => :saturday
80
+ }
81
+
82
+ day_sequence = {
83
+ :sunday => 0,
84
+ :monday => 1,
85
+ :tuesday => 2,
86
+ :wednesday => 3,
87
+ :thursday => 4,
88
+ :friday => 5,
89
+ :saturday => 6
90
+ }
91
+
92
+ scanner.each do |scanner_item, day|
93
+ position_in_sequence = day_sequence[day]
94
+ token.update(:weekday, scanner[scanner_item], 7, position_in_sequence) if scanner_item =~ token.word
95
+ end
96
+ token
97
+ end
98
+
99
+ def self.scan_for_special_text(token)
100
+ scanner = {
101
+ /^other$/ => :other,
102
+ /^begin(ing|ning)?$/ => :beginning,
103
+ /^start$/ => :beginning,
104
+ /^end$/ => :end,
105
+ /^mid(d)?le$/ => :middle}
106
+ scanner.keys.each do |scanner_item|
107
+ token.update(:special, scanner[scanner_item], 7) if scanner_item =~ token.word
108
+ end
109
+ token
110
+ end
111
+
112
+ def self.scan_for_units(token)
113
+ scanner = {
114
+ /^year(ly)?s?|annually$/ => {:type => :year, :interval => 365, :start => :today},
115
+ /^month(ly)?s?$/ => {:type => :month, :interval => 30, :start => :today},
116
+ /^fortnights?$/ => {:type => :fortnight, :interval => 365, :start => :today},
117
+ /^week(ly)?s?$/ => {:type => :week, :interval => 7, :start => :today},
118
+ /^weekends?$/ => {:type => :weekend, :interval => 7, :start => :saturday},
119
+ /^days?$/ => {:type => :day, :interval => 1, :start => :today},
120
+ /^daily?$/ => {:type => :day, :interval => 1, :start => :today},
121
+ /^minutes?$/ => {:type => :minute_word, :start => :today},
122
+ /^hour(ly)?s?$/ => {:type => :hour, :start => :today},
123
+ /^am|pm$/ => {:type => :meridiem}
124
+ }
125
+ scanner.keys.each do |scanner_item|
126
+ if scanner_item =~ token.word
127
+ token.update(scanner[scanner_item][:type], scanner[scanner_item][:start], scanner[scanner_item][:interval]) if scanner_item =~ token.word
128
+ end
129
+ end
130
+ token
131
+ end
132
+
133
+ # Split time tokens into constituent hour and minute tokens
134
+ def self.split_time_tokens(tokens)
135
+ tokens_output = []
136
+ tokens.each do |token|
137
+ if (token.type == :time)
138
+ time_parts = token.word.split(':')
139
+ hour_token = Midnight::Token.new(time_parts[0].to_i)
140
+ minute_token = Midnight::Token.new(time_parts[1].to_i)
141
+ hour_token.update(:hour)
142
+ minute_token.update(:minute)
143
+ tokens_output << hour_token
144
+ tokens_output << minute_token
145
+ else
146
+ tokens_output << token
147
+ end
148
+ end
149
+
150
+ tokens_output
151
+ end
152
+
153
+ end
@@ -0,0 +1,3 @@
1
+ module Midnight
2
+ VERSION = '0.0.1.pre'
3
+ end
@@ -0,0 +1,103 @@
1
+ require 'strscan'
2
+
3
+ class Numerizer
4
+
5
+ DIRECT_NUMS = [
6
+ ['eleven', '11'],
7
+ ['twelve', '12'],
8
+ ['thirteen', '13'],
9
+ ['fourteen', '14'],
10
+ ['fifteen', '15'],
11
+ ['sixteen', '16'],
12
+ ['seventeen', '17'],
13
+ ['eighteen', '18'],
14
+ ['nineteen', '19'],
15
+ ['ninteen', '19'], # Common mis-spelling
16
+ ['zero', '0'],
17
+ ['one', '1'],
18
+ ['two', '2'],
19
+ ['three', '3'],
20
+ ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty
21
+ ['five', '5'],
22
+ ['six(\W|$)', '6\1'],
23
+ ['seven(\W|$)', '7\1'],
24
+ ['eight(\W|$)', '8\1'],
25
+ ['nine(\W|$)', '9\1'],
26
+ ['ten', '10'],
27
+ ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
28
+ ]
29
+
30
+ TEN_PREFIXES = [ ['twenty', 20],
31
+ ['thirty', 30],
32
+ ['fourty', 40],
33
+ ['fifty', 50],
34
+ ['sixty', 60],
35
+ ['seventy', 70],
36
+ ['eighty', 80],
37
+ ['ninety', 90]
38
+ ]
39
+
40
+ BIG_PREFIXES = [ ['hundred', 100],
41
+ ['thousand', 1000],
42
+ ['million', 1_000_000],
43
+ ['billion', 1_000_000_000],
44
+ ['trillion', 1_000_000_000_000],
45
+ ]
46
+
47
+ class << self
48
+ def numerize(string)
49
+ string = string.dup
50
+
51
+ # preprocess
52
+ string.gsub!(/ +|([^\d])-([^d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
53
+ string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
54
+
55
+ # easy/direct replacements
56
+
57
+ DIRECT_NUMS.each do |dn|
58
+ string.gsub!(/#{dn[0]}/i, dn[1])
59
+ end
60
+
61
+ # ten, twenty, etc.
62
+
63
+ TEN_PREFIXES.each do |tp|
64
+ string.gsub!(/(?:#{tp[0]})( *\d(?=[^\d]|$))*/i) { (tp[1] + $1.to_i).to_s }
65
+ end
66
+
67
+ # hundreds, thousands, millions, etc.
68
+
69
+ BIG_PREFIXES.each do |bp|
70
+ string.gsub!(/(\d*) *#{bp[0]}/i) { (bp[1] * $1.to_i).to_s}
71
+ andition(string)
72
+ #combine_numbers(string) # Should to be more efficient way to do this
73
+ end
74
+
75
+ # fractional addition
76
+ # I'm not combining this with the previous block as using float addition complicates the strings
77
+ # (with extraneous .0's and such )
78
+ string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
79
+
80
+ string
81
+ end
82
+
83
+ private
84
+ def andition(string)
85
+ sc = StringScanner.new(string)
86
+ while(sc.scan_until(/(\d+)( | and )(\d+)(?=[^\w]|$)/i))
87
+ if sc[2] =~ /and/ || sc[1].size > sc[3].size
88
+ string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[3].to_i).to_s
89
+ sc.reset
90
+ end
91
+ end
92
+ end
93
+
94
+ # def combine_numbers(string)
95
+ # sc = StringScanner.new(string)
96
+ # while(sc.scan_until(/(\d+)(?: | and |-)(\d+)(?=[^\w]|$)/i))
97
+ # string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[2].to_i).to_s
98
+ # sc.reset
99
+ # end
100
+ # end
101
+
102
+ end
103
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'midnight/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "midnight"
8
+ spec.version = Midnight::VERSION
9
+ spec.authors = ["bluefuton"]
10
+ spec.email = ["chris@bluefuton.com"]
11
+ spec.summary = %q{Parse natural language date/time into a cron expression}
12
+ spec.description = ''
13
+ spec.homepage = 'https://github.com/bluefuton/midnight'
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake", '~> 0'
23
+ spec.add_dependency 'chronic', '=0.10.2'
24
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+
7
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'midnight')
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+ require 'time'
3
+ require 'test/unit'
4
+ require './lib/midnight/cron_expression'
5
+
6
+ class TestParsing < Test::Unit::TestCase
7
+
8
+ def setup
9
+
10
+ end
11
+
12
+ def test_cron_expression
13
+ cron = Midnight::CronExpression.new
14
+ cron.minute = 30
15
+ assert_equal '30 * * * *', cron.to_s
16
+ assert_equal 30, cron.minute
17
+ end
18
+ end
@@ -0,0 +1,68 @@
1
+ require 'helper'
2
+ require 'time'
3
+ require 'test/unit'
4
+
5
+ class TestParsing < Test::Unit::TestCase
6
+
7
+ def test_parse
8
+ expected_results = {
9
+ 'each day' => '0 0 * * *',
10
+ 'every day' => '0 0 * * *',
11
+ 'daily' => '0 0 * * *',
12
+ 'every day at 3am' => '0 3 * * *',
13
+ 'daily at 5am' => '0 5 * * *',
14
+ 'every week' => '0 0 * * 0',
15
+ 'weekly' => '0 0 * * 0',
16
+ 'every minute' => '* * * * *',
17
+ 'every 5 minutes' => '*/5 * * * *',
18
+ 'every 30 minutes' => '*/30 * * * *',
19
+ 'every month' => '0 0 1 * *',
20
+ 'monthly' => '0 0 1 * *',
21
+ 'every Monday' => '0 0 * * 1',
22
+ 'every Wednesday' => '0 0 * * 3',
23
+ 'every Friday' => '0 0 * * 5',
24
+ 'this one should not return a result' => nil,
25
+ 'every hour' => '0 * * * *',
26
+ 'every 6 hours' => '0 */6 * * *',
27
+ 'hourly' => '0 * * * *',
28
+ 'every year' => '0 0 1 1 *',
29
+ 'yearly' => '0 0 1 1 *',
30
+ 'annually' => '0 0 1 1 *',
31
+ 'every day at 9am' => '0 9 * * *',
32
+ 'every day at 5pm' => '0 17 * * *',
33
+ 'every day at 5:45pm' => '45 17 * * *',
34
+ 'every day at 17:00' => '0 17 * * *',
35
+ 'every day at 17:25' => '25 17 * * *',
36
+ '5:15am every Tuesday' => '15 5 * * 2',
37
+ '7pm every Thursday' => '0 19 * * 4',
38
+ 'every May' => '0 0 1 5 *',
39
+ 'every December' => '0 0 1 12 *',
40
+ 'midnight' => '0 0 * * *',
41
+ 'midnight on tuesdays' => '0 0 * * 2',
42
+ 'every 5 minutes on Tuesdays' => '*/5 * * * 2',
43
+ 'every other day' => nil, # other is currently unsupported
44
+ 'noon' => '0 12 * * *'
45
+ }
46
+
47
+ expected_results.each do |search,cron_string|
48
+ assert_equal(cron_string, Midnight.parse(search).to_s, search)
49
+ end
50
+ end
51
+
52
+ def test_argument_validation
53
+ assert_raise(Midnight::InvalidArgumentException) do
54
+ time = Midnight.parse("may 27", :today => 'something odd')
55
+ end
56
+
57
+ assert_raise(Midnight::InvalidArgumentException) do
58
+ time = Midnight.parse("may 27", :foo => :bar)
59
+ end
60
+ end
61
+
62
+ private
63
+ def parse_now(string, options={})
64
+ out = Midnight.parse(string, {}.merge(options))
65
+ puts ("Midnight.parse('#{string}') #=> #{out}")
66
+ out
67
+ end
68
+ end
@@ -0,0 +1,21 @@
1
+ every 3 days
2
+ every 3 weeks
3
+ every 3 months
4
+ every 3 years
5
+
6
+ every other day
7
+ every other week
8
+ every other month
9
+ every other year
10
+ every other day starting May 1st
11
+ every other week starting this Sunday
12
+
13
+ the 10th of the month
14
+ the tenth of the month
15
+ the 3rd Sunday of the month
16
+ out of bounds values e.g. 4:61pm
17
+ midday - 'noon' works, so Chronic could be patched
18
+ every 5 minutes on fridays
19
+ multiple days "every tuesday, wednesday and friday at 5pm"
20
+ multiple hours or minutes
21
+ ranges of minutes
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: midnight
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre
5
+ platform: ruby
6
+ authors:
7
+ - bluefuton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: chronic
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.10.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.2
55
+ description: ''
56
+ email:
57
+ - chris@bluefuton.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rubocop_todo.yml"
64
+ - ".travis.yml"
65
+ - Gemfile
66
+ - LICENSE
67
+ - LICENSE.txt
68
+ - README.md
69
+ - README.textile
70
+ - Rakefile
71
+ - lib/midnight.rb
72
+ - lib/midnight/converter.rb
73
+ - lib/midnight/cron_expression.rb
74
+ - lib/midnight/handler.rb
75
+ - lib/midnight/midnight.rb
76
+ - lib/midnight/repeater.rb
77
+ - lib/midnight/version.rb
78
+ - lib/numerizer/numerizer.rb
79
+ - midnight.gemspec
80
+ - test/helper.rb
81
+ - test/test_cron_expression.rb
82
+ - test/test_parsing.rb
83
+ - todo.txt
84
+ homepage: https://github.com/bluefuton/midnight
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">"
100
+ - !ruby/object:Gem::Version
101
+ version: 1.3.1
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.2.2
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Parse natural language date/time into a cron expression
108
+ test_files:
109
+ - test/helper.rb
110
+ - test/test_cron_expression.rb
111
+ - test/test_parsing.rb
112
+ has_rdoc: