store_hours 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +94 -0
- data/Rakefile +8 -0
- data/lib/store_hours.rb +141 -0
- data/lib/store_hours/common_methods.rb +33 -0
- data/lib/store_hours/constants.rb +6 -0
- data/lib/store_hours/semantic_error.rb +12 -0
- data/lib/store_hours/text_input_parser.rb +54 -0
- data/lib/store_hours/tree_transformer.rb +118 -0
- data/lib/store_hours/version.rb +3 -0
- data/store_hours.gemspec +21 -0
- data/test/store_hours_test.rb +161 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MWJjOWYzNjliZWE5YWM1MjY3ZWM3MWI0NDE0NDljZDA3NzY1MWExZg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ODc4YTE1YjVmZGQ2Y2Q0NDIyZGVlZGQ4NjQ5MzA5MTkzNDNmYzQ1MA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZTA3MTg3MDhiMDg1MjJkZDRiMDY5NTYzNWFiNGQ2ZmE0NmQ3YjRiOThkYjNm
|
10
|
+
YjliZTI1MDgxYTYyYTIxOGUwMTk2ZjczNThkNWUyOWI5ZGQ5M2JlYjUwNTBl
|
11
|
+
NDc4YjI2OGIxOTIwYjRjNTFlNjBlMTQ5YTI2MjZkZmZkYWRkZDM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YmY4M2RkNDg0ZWQ1ZmFkODc4NzlmMTc0MTEwMzdlMDM4ZjRjM2VmNjY1NGMx
|
14
|
+
N2I4MGEwOTNmNjg4NmUzM2Y1YWY3ZjA3ZWRmZjQ1Y2E0MWUyNDdhNzllMmM2
|
15
|
+
Mjg2YTgxM2NlYTdhY2U3NjkwMmVhNTY2NzEwODc5NGUyZDcyYzM=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Yanhao Zhu
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# StoreHours
|
2
|
+
|
3
|
+
A very simple parser to parse text like
|
4
|
+
|
5
|
+
Mon-Fri: 9AM-5PM
|
6
|
+
Sat: 10AM-7PM
|
7
|
+
Sun: closed
|
8
|
+
|
9
|
+
and build an internal data structure to enable possible formatting and queries.
|
10
|
+
|
11
|
+
This class is designed for situations where (1) you like to use a single text field in database to store open hours, and (2) you would like to be able to check whether the store opens for a certain time, or to make sure inputs are valid, or to display the hours in a format different from user input (for example, format the input in html to display on website).
|
12
|
+
|
13
|
+
Here is an example about how to use this class in rails. Suppose you have a model
|
14
|
+
called "Store" with a text filed named normal_business_hours, you can add this validation
|
15
|
+
method:
|
16
|
+
|
17
|
+
validate :normal_business_hours_must_be_in_valid_format
|
18
|
+
def normal_business_hours_must_be_in_valid_format
|
19
|
+
hours_parser = ::StoreHours::StoreHours.new
|
20
|
+
#check whether input is valid?
|
21
|
+
success, error_message = hours_parser.from_text(self.normal_business_hours)
|
22
|
+
if !success
|
23
|
+
#input is not valid
|
24
|
+
errors.add(:normal_business_hours, error_message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Examples of valid input:
|
29
|
+
|
30
|
+
Mon: 8AM-12PM 2PM-6PM
|
31
|
+
Tue: 8:30AM-5:30PM
|
32
|
+
Wed-Fri: 9:00AM - 4:50PM 6:00PM-10:30PM
|
33
|
+
Sat: 10AM-1PM 1:30PM - 6PM
|
34
|
+
Sun: closed
|
35
|
+
mon: 10:00am - 5:00PM #case insensitive
|
36
|
+
mon: 8:00am-12:00pm, 1pm-5pm #time periods can be separated by comma(,) or space
|
37
|
+
mon: 8:00am-12:00pm 1pm-5pm
|
38
|
+
mon-fri : 10am-5pm
|
39
|
+
sat - sun: closed
|
40
|
+
sun : closed
|
41
|
+
|
42
|
+
Examples of invalid entries:
|
43
|
+
|
44
|
+
mon 10am - 5pm # colon(:) after week day(s) is required
|
45
|
+
mon fri: 10am - 5pm # dash(-) between two days is required
|
46
|
+
mon-fri: 10:am - 5pm # minute component for time is required when the colon(:) is present
|
47
|
+
mon-fri: 10 am - 5 pm # no space is allowed between time digits and am/pm
|
48
|
+
mon : 10am - 17 # standard time format (with am or pm) is required
|
49
|
+
sat-sun: 10am-1pm closed # closed can only be used with other time periods
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
```
|
54
|
+
#!ruby
|
55
|
+
1.9.3-p194 :001 > require 'store_hours'
|
56
|
+
=> true
|
57
|
+
1.9.3-p194 :002 > hours_parser = ::StoreHours::StoreHours.new
|
58
|
+
=> #<StoreHours::StoreHours:0x007fb5dc1bead8 @hours=[]>
|
59
|
+
1.9.3-p194 :003 > hours_parser.from_text('mon:10:40am-5pm tue:8am-')
|
60
|
+
=> [false, "syntax error: input is not in correct format"]
|
61
|
+
1.9.3-p194 :004 > hours_parser.from_text('mon:10:40am-5pm tue:8am-6pm')
|
62
|
+
=> [true, ""]
|
63
|
+
1.9.3-p194 :006 > puts hours_parser.to_text
|
64
|
+
Mon: 10:40AM - 5:00PM
|
65
|
+
Tue: 8:00AM - 6:00PM
|
66
|
+
1.9.3-p194 :007 > hours_parser
|
67
|
+
=> #<StoreHours::StoreHours:0x007fb5dc1bead8 @hours=[{1..1=>[640..1020]}, {2..2=>[480..1080]}]>
|
68
|
+
```
|
69
|
+
|
70
|
+
## Limitations
|
71
|
+
|
72
|
+
Time periods can only be within 0AM-11:59PM. In other words, time periods like 11:30PM-6:00AM are not supported.
|
73
|
+
|
74
|
+
## Installation
|
75
|
+
|
76
|
+
Add this line to your application's Gemfile:
|
77
|
+
|
78
|
+
gem 'store_hours'
|
79
|
+
|
80
|
+
And then execute:
|
81
|
+
|
82
|
+
$ bundle
|
83
|
+
|
84
|
+
Or install it yourself as:
|
85
|
+
|
86
|
+
$ gem install store_hours
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
1. Fork it
|
91
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
92
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
93
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
94
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/store_hours.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
require "store_hours/version"
|
4
|
+
require "store_hours/text_input_parser"
|
5
|
+
require "store_hours/tree_transformer"
|
6
|
+
require "store_hours/semantic_error"
|
7
|
+
require "store_hours/common_methods"
|
8
|
+
|
9
|
+
require "json"
|
10
|
+
|
11
|
+
module StoreHours
|
12
|
+
# A very simple parser to parse text like
|
13
|
+
# Mon-Fri: 9AM-5PM
|
14
|
+
# Sat: 10AM-7PM
|
15
|
+
# Sun: closed
|
16
|
+
# and build an internal data structure to enable possible formatting and queries.
|
17
|
+
#
|
18
|
+
# This class is designed to use when (1) you like to use a single text field in
|
19
|
+
# database to store open hours, and (2) you would like to be able to check whether
|
20
|
+
# the store opens for a certain time, or to make sure inputs are valid, or to
|
21
|
+
# display the hours in a format different from user input (for example, take plain
|
22
|
+
# text from users, but to format the input in html to display).
|
23
|
+
#
|
24
|
+
# Here is an example about how to use this class in rails. Suppose you have a model
|
25
|
+
# called "Store" with a text filed named normal_business_hours, you can add this validation
|
26
|
+
# method:
|
27
|
+
#
|
28
|
+
# validate :normal_business_hours_must_be_in_valid_format
|
29
|
+
# def normal_business_hours_must_be_in_valid_format
|
30
|
+
# hours_parser = ::StoreHours::StoreHours.new
|
31
|
+
# #check whether input is valid?
|
32
|
+
# success, error_message = hours_parser.from_text(self.normal_business_hours)
|
33
|
+
# if !success
|
34
|
+
# #input is not valid
|
35
|
+
# errors.add(:normal_business_hours, error_message)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# Please refer to text_input_parser.rb and tree_transformer.rb to get some idea
|
40
|
+
# of what kinds of inputs are valid.
|
41
|
+
#
|
42
|
+
class StoreHours
|
43
|
+
def initialize
|
44
|
+
@hours = []
|
45
|
+
end
|
46
|
+
|
47
|
+
# Try to parse the input text.
|
48
|
+
# @param text [String] store hours text input, case-insensitive
|
49
|
+
# @return [Boolean, String] returns [true, ''] if the text is valid, otherwise,
|
50
|
+
# the return value will be [false, "error message"]
|
51
|
+
#
|
52
|
+
# This method will build the internal data structure for valid text argument.
|
53
|
+
#
|
54
|
+
# Please don't ignore the return value from this method as it is the only way to
|
55
|
+
# know whether input is valid.
|
56
|
+
#
|
57
|
+
def from_text(text)
|
58
|
+
text = '' if text == nil
|
59
|
+
result = true
|
60
|
+
error_message = ''
|
61
|
+
|
62
|
+
begin
|
63
|
+
# parse the text into an intermediary tree
|
64
|
+
# this call may raise Parslet::ParseFailed exception
|
65
|
+
tree = TextInputParser.new.parse(text.strip.downcase)
|
66
|
+
|
67
|
+
# convert the tree into internal data structure
|
68
|
+
# please refer to tree_transformer.rb for the details of this structure
|
69
|
+
# this call may raise StoreHours::SemanticError exception
|
70
|
+
@hours = TreeTransformer.new.apply(tree)
|
71
|
+
|
72
|
+
result = true
|
73
|
+
rescue Parslet::ParseFailed => e
|
74
|
+
puts e.cause.ascii_tree
|
75
|
+
puts e.cause.message
|
76
|
+
puts text[0..e.cause.source.chars_left]
|
77
|
+
|
78
|
+
result = false
|
79
|
+
error_message = "syntax error: input is not in correct format"
|
80
|
+
rescue ::StoreHours::SemanticError => e
|
81
|
+
puts e.message
|
82
|
+
|
83
|
+
result = false
|
84
|
+
error_message = e.message
|
85
|
+
end
|
86
|
+
|
87
|
+
return [result, error_message]
|
88
|
+
end
|
89
|
+
|
90
|
+
# This is the method you can use to display store hours.
|
91
|
+
def to_text
|
92
|
+
text = ''
|
93
|
+
@hours.each do |days_table|
|
94
|
+
days = days_table.keys.first #days is the day range, for example, (1..5)
|
95
|
+
if block_given?
|
96
|
+
text += yield NUM_TO_WEEKDAY[days.first], NUM_TO_WEEKDAY[days.last], days_table[days]
|
97
|
+
else
|
98
|
+
text += NUM_TO_WEEKDAY[days.first].to_s
|
99
|
+
text += "-" + NUM_TO_WEEKDAY[days.last].to_s if days.first != days.last
|
100
|
+
text += ": "
|
101
|
+
|
102
|
+
days_table[days].each_with_index do |minutes, index|
|
103
|
+
text += ', ' if index > 0
|
104
|
+
if minutes.first == -1 #closed days
|
105
|
+
text += "closed"
|
106
|
+
elsif
|
107
|
+
text += ::StoreHours::from_minutes_to_time_str(minutes.first) + " - " + ::StoreHours::from_minutes_to_time_str(minutes.last)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
text += "\n"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
text.strip
|
115
|
+
end
|
116
|
+
|
117
|
+
def is_open?(t)
|
118
|
+
@hours.each do |days_table|
|
119
|
+
# days_table in the format of range(wday..wday) => [range(minutes..minutes),...]
|
120
|
+
# only one key in the hash table
|
121
|
+
days = days_table.keys.first
|
122
|
+
if days.include?(t.wday == 0 ? 7 : t.wday)
|
123
|
+
days_table[days].each do |min_range|
|
124
|
+
minutes = t.hour * 60 + t.min
|
125
|
+
|
126
|
+
if min_range.include?(minutes)
|
127
|
+
return true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
return false
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module StoreHours
|
2
|
+
|
3
|
+
# Convert a time point in a day to the number of minutes passed since midnight.
|
4
|
+
# @param hour [Fixnum] the hour component of time (not in military format)
|
5
|
+
# @param minutes [Fixnum] the minutes component of time
|
6
|
+
# @param am_or_pm [Symbol, :am or :pm] morning or afternoon
|
7
|
+
# @return [Fixnum] the number of minutes passed since midnight
|
8
|
+
#
|
9
|
+
def self.convert_time_input_to_minutes(hour, minutes, am_or_pm)
|
10
|
+
hour += 12 if am_or_pm == :pm and hour < 12
|
11
|
+
hour = 0 if am_or_pm == :am and hour == 12
|
12
|
+
|
13
|
+
hour * 60 + minutes
|
14
|
+
end
|
15
|
+
|
16
|
+
# This is the reverse function of convert_time_input_to_minutes(hour, minutes,
|
17
|
+
# am_or_pm).
|
18
|
+
#
|
19
|
+
# This function coverts the number of minutes passed since midnight to a
|
20
|
+
# standard time string like 10:00AM.
|
21
|
+
# @param t [Fixnum] the number of minutes passed since midnight
|
22
|
+
# @return [String] the standard time string
|
23
|
+
#
|
24
|
+
def self.from_minutes_to_time_str(t)
|
25
|
+
hour_part = t / 60
|
26
|
+
minute_part = t % 60
|
27
|
+
|
28
|
+
am_or_pm = (hour_part >= 12 ? 'PM' : 'AM')
|
29
|
+
hour_part = hour_part - 12 if hour_part >= 13
|
30
|
+
|
31
|
+
"#{ hour_part }:#{ format('%02d', minute_part) }#{ am_or_pm }"
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module StoreHours
|
2
|
+
# When parslet apply the TreeTransformer to the intermediary tree generated by parser,
|
3
|
+
# it may raise this exception if the input is valid syntax-wise but contains errors.
|
4
|
+
#
|
5
|
+
# Possible scenarios to cause this exception are defined in tree_transformer.rb.
|
6
|
+
#
|
7
|
+
class SemanticError < StandardError
|
8
|
+
def initialize(message)
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module StoreHours
|
4
|
+
# Parser definition for store hours.
|
5
|
+
#
|
6
|
+
# A valid input includes one or more entries of durations separated by one or more white spaces.
|
7
|
+
#
|
8
|
+
# This parser will only takes lower cases. However,
|
9
|
+
#
|
10
|
+
# Each entry has two parts: (1)week day or week days, and (2) one or more time periods when the
|
11
|
+
# store is open, or closed for the days the store closes.
|
12
|
+
#
|
13
|
+
# Examples of valid entries:
|
14
|
+
# mon: 10:00am - 5:00pm
|
15
|
+
# mon: 8:00am-12:00pm, 1pm-5pm #time periods can be separated by comma(,) or space
|
16
|
+
# mon: 8:00am-12:00pm 1pm-5pm
|
17
|
+
# mon-fri : 10am-5pm
|
18
|
+
# sat - sun: closed
|
19
|
+
# sun : closed
|
20
|
+
#
|
21
|
+
# Examples of invalid entries:
|
22
|
+
# mon 10am - 5pm # colon(:) after week day(s) is required
|
23
|
+
# mon fri: 10am - 5pm # dash(-) between two days is required
|
24
|
+
# mon-fri: 10:am - 5pm # minute component for time is required when the colon(:) is present
|
25
|
+
# mon-fri: 10 am - 5 pm # no space is allowed between time digits and am/pm
|
26
|
+
# mon : 10am - 17 # standard time format (with am or pm) is required
|
27
|
+
# sat-sun: 10am-1pm closed # closed can only be used with other time periods
|
28
|
+
#
|
29
|
+
class TextInputParser < Parslet::Parser
|
30
|
+
rule(:space) { match('\s').repeat }
|
31
|
+
rule(:sep) { str('-') }
|
32
|
+
rule(:colon) { str(':') }
|
33
|
+
rule(:comma) { str(',') }
|
34
|
+
rule(:ampm) { str('am') | str('pm') }
|
35
|
+
rule(:closed) { str('closed') }
|
36
|
+
|
37
|
+
rule(:day) { (str('mon') | str('tue') | str('wed') | str('thu') | str('fri') | str('sat') | str('sun')).as(:day) }
|
38
|
+
|
39
|
+
rule(:range) { day.as(:day_from) >> space >> sep >> space >> day.as(:day_to) }
|
40
|
+
|
41
|
+
rule(:left) { range.as(:day_range) | day.as(:day_single) }
|
42
|
+
|
43
|
+
rule(:minutes) { colon >> match('[0-9]').repeat(1,2).as(:minute) }
|
44
|
+
rule(:time) { match('[0-9]').repeat(1,2).as(:hour) >> minutes.maybe >> ampm.as(:ampm) }
|
45
|
+
rule(:trange) { time.as(:time_from) >> space >> sep >> space >> time.as(:time_to) >> space >> comma.maybe >> space }
|
46
|
+
rule(:right) { trange.repeat(1).as(:time_range) | (closed.as(:closed) >> space) }
|
47
|
+
|
48
|
+
rule(:line) { left.as(:line_left) >> space >> colon >> space >> right.as(:line_right) }
|
49
|
+
|
50
|
+
rule(:lines) { line.repeat(1).as(:lines) }
|
51
|
+
|
52
|
+
root(:lines)
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
require 'store_hours/constants'
|
4
|
+
require 'store_hours/semantic_error'
|
5
|
+
require 'store_hours/common_methods'
|
6
|
+
|
7
|
+
module StoreHours
|
8
|
+
# Convert the intermediary tree resulted from parser to the internal data structure in the format of
|
9
|
+
# [{d1..d2 => [(m1..m2), (m3..m4)]},{d3..d3 => [(m5..m6)]}]
|
10
|
+
# where
|
11
|
+
# d is the integer value of week day (i.e, Mon = 1, Tue = 2, ..., Sun = 7),
|
12
|
+
# m is the number of minutes passed from midnight (12AM).
|
13
|
+
#
|
14
|
+
# For example, "Mon-Fri: 1AM-2AM, 10AM-5PM Sat-Sun: closed" will be converted to
|
15
|
+
# [{1..5 => [(60..120), (600..1020)]}, {6..7 => [(-1..-1)]}].
|
16
|
+
#
|
17
|
+
# Use the list of single entry hash tables instead of one hash table to preserve order of inputs.
|
18
|
+
#
|
19
|
+
class TreeTransformer < Parslet::Transform
|
20
|
+
rule(:hour => simple(:h), :ampm => simple(:ap)) { |dict|
|
21
|
+
::StoreHours::convert_time_input_to_minutes(dict[:h].to_i, 0, dict[:ap].to_sym)
|
22
|
+
}
|
23
|
+
rule(:hour => simple(:h), :minute => simple(:m), :ampm => simple(:ap)) { |dict|
|
24
|
+
::StoreHours::convert_time_input_to_minutes(dict[:h].to_i, dict[:m].to_i, dict[:ap].to_sym)
|
25
|
+
}
|
26
|
+
rule(:closed => simple(:x)) { x.to_sym }
|
27
|
+
rule(:time_from => simple(:f), :time_to => simple(:t)) { |dict| check_starting_time_not_later_than_ending_time(dict[:f], dict[:t]) }
|
28
|
+
rule(:time_range => sequence(:x)) { |dict| check_no_overlap_within_time_periods_for_single_day_range(dict[:x]) }
|
29
|
+
rule(:day => simple(:x)) { WEEKDAY_TO_NUM[x.to_sym]}
|
30
|
+
rule(:day_single => simple(:x)) { x..x }
|
31
|
+
rule(:day_range => {:day_from => simple(:f), :day_to => simple(:t)}) { |dict| check_starting_day_not_later_than_ending_day(dict[:f], dict[:t]) }
|
32
|
+
rule(:line_left => simple(:d), :line_right => sequence(:t)) { { d => t} }
|
33
|
+
rule(:line_left => simple(:d), :line_right => simple(:c)) { {d => [-1..-1]} } #for "closed" days
|
34
|
+
rule(:lines => subtree(:x)) { |dict| check_no_overlap_within_day_ranges(dict[:x]) }
|
35
|
+
|
36
|
+
private
|
37
|
+
# Make sure the starting time is not later than the ending time.
|
38
|
+
# @param starting_time [Fixnum] starting time in minutes passed since midnight
|
39
|
+
# @param ending_time [Fixnum] ending time in minutes
|
40
|
+
# @return [Range, (starting_time..ending_time)] if it is valid
|
41
|
+
#
|
42
|
+
# Will raise SemanticError if it is invalid.
|
43
|
+
#
|
44
|
+
def self.check_starting_time_not_later_than_ending_time(starting_time, ending_time)
|
45
|
+
if starting_time > ending_time
|
46
|
+
raise ::StoreHours::SemanticError.new "incorrect time period specified: ending time has to be later"
|
47
|
+
end
|
48
|
+
starting_time..ending_time
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check to make sure the day range is valid. The integer values of week days are defined in constants.rb.
|
52
|
+
# @param starting_day [Fixnum] starting day of the range, mon=1, ..., sun=7
|
53
|
+
# @param ending_day [Fixnum] ending day of the range
|
54
|
+
# @return [Range, (starting_day..ending_day)]
|
55
|
+
#
|
56
|
+
# Will raise SemanticError if starting_day is later than ending_day.
|
57
|
+
#
|
58
|
+
def self.check_starting_day_not_later_than_ending_day(starting_day, ending_day)
|
59
|
+
if starting_day > ending_day
|
60
|
+
raise ::StoreHours::SemanticError.new "incorrect day range specified: ending day has to be later"
|
61
|
+
end
|
62
|
+
starting_day..ending_day
|
63
|
+
end
|
64
|
+
|
65
|
+
# For a give day or day range, check to make sure there are no overlap within time periods
|
66
|
+
# @param periods [Array of Range] time periods for a single day range, i.e., [(60, 120), (540, 1020)]
|
67
|
+
# @return [Array of Range] the same as argument periods
|
68
|
+
#
|
69
|
+
# Will raise SemanticError if there is overlap.
|
70
|
+
#
|
71
|
+
def self.check_no_overlap_within_time_periods_for_single_day_range(periods)
|
72
|
+
if self.ranges_overlap?(periods)
|
73
|
+
raise ::StoreHours::SemanticError.new "incorrect time range specified: overlap for a single day range"
|
74
|
+
end
|
75
|
+
|
76
|
+
periods
|
77
|
+
end
|
78
|
+
|
79
|
+
# For the overall tree, check to make sure that a day integer can only appear once.
|
80
|
+
# @param tree [Array of Hashtable] the already transformed internal data presentation.
|
81
|
+
# @return [Array of Hashtable] the tree passed in will be untouched
|
82
|
+
#
|
83
|
+
# Will raise SemanticError if there is overlap
|
84
|
+
#
|
85
|
+
def self.check_no_overlap_within_day_ranges(tree)
|
86
|
+
# make an array of hashtable keys
|
87
|
+
ranges = []
|
88
|
+
tree.each do |table|
|
89
|
+
ranges << table.keys.first
|
90
|
+
end
|
91
|
+
|
92
|
+
if self.ranges_overlap?(ranges)
|
93
|
+
raise ::StoreHours::SemanticError.new "incorrect day range specified: at last one day appear more than once"
|
94
|
+
end
|
95
|
+
|
96
|
+
tree
|
97
|
+
end
|
98
|
+
|
99
|
+
# Check whether two or more ranges contain overlaps.
|
100
|
+
# @param periods [Array of Range] list of ranges to be checked against
|
101
|
+
# @return [Boolean] return true if ranges overlap
|
102
|
+
#
|
103
|
+
def self.ranges_overlap?(periods)
|
104
|
+
# sort the ranges by range's first item
|
105
|
+
sorted_periods = periods.sort {|x, y| x.first <=> y.first }
|
106
|
+
|
107
|
+
# starts from the second item
|
108
|
+
last_index = periods.length - 1
|
109
|
+
for i in 1..last_index
|
110
|
+
if sorted_periods[i].first <= sorted_periods[i-1].last
|
111
|
+
return true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/store_hours.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'store_hours/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "store_hours"
|
8
|
+
gem.version = StoreHours::VERSION
|
9
|
+
gem.authors = ["Yanhao Zhu"]
|
10
|
+
gem.email = ["yanhaozhu@gmail.com"]
|
11
|
+
gem.description = 'A small parser for store normal business hours'
|
12
|
+
gem.summary = 'A small parser for store normal business hours'
|
13
|
+
gem.homepage = "https://github.com/luanzhu/store_hours"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'parslet', '~> 1.5.0'
|
21
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'minitest/spec'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
require 'store_hours'
|
5
|
+
|
6
|
+
|
7
|
+
describe StoreHours::StoreHours do
|
8
|
+
before { @h = StoreHours::StoreHours.new }
|
9
|
+
|
10
|
+
it "should return empty string for to_text() without loading from text" do
|
11
|
+
@h.to_text.must_equal ''
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should parse store hours text and format text" do
|
15
|
+
text = "Mon - Thu : 9:00AM - 5:00PM, 6PM-9PM Fri: 9AM - 10PM
|
16
|
+
Sun: closed"
|
17
|
+
r, msg = @h.from_text(text)
|
18
|
+
|
19
|
+
r.must_equal true
|
20
|
+
@h.to_text.must_equal "Mon-Thu: 9:00AM - 5:00PM, 6:00PM - 9:00PM\nFri: 9:00AM - 10:00PM\nSun: closed"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return true for valid text" do
|
24
|
+
texts = ["mon: 10:00am - 5:00pm", "mon-fri : 10am-5pm sat - sun: closed", "sun : closed Sat: 10am - 3:30pm 5pm-11pm"]
|
25
|
+
|
26
|
+
texts.each do |t|
|
27
|
+
r, msg = @h.from_text(t)
|
28
|
+
|
29
|
+
r.must_equal true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return false for invalid text" do
|
34
|
+
texts = ["M-T 9:00am-5pm", "mon 10am - 5pm", "mon-fri: 10:am - 5pm", "mon-fri: 10 am - 5 pm", "mon : 10am - 17",
|
35
|
+
"sat-sun: 10am-1pm closed", "sat-sun: closed 10am-1pm", "mon fri: 10am - 5pm"
|
36
|
+
]
|
37
|
+
|
38
|
+
texts.each do |t|
|
39
|
+
@h.from_text(t)[0].must_equal false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should check whether the store is open for a time object" do
|
44
|
+
text = "Mon-Fri: 8AM - 12PM, 1pm-5pm\nSat-Sun: closed"
|
45
|
+
|
46
|
+
r, msg = @h.from_text(text)
|
47
|
+
|
48
|
+
r.must_equal true
|
49
|
+
|
50
|
+
t = Time.new(2013, 2, 24, 11, 0) #2013 Feb 24, 11:00AM, Sunday
|
51
|
+
@h.is_open?(t).must_equal false
|
52
|
+
|
53
|
+
t = Time.new(2013, 2, 23, 17, 0) #2013 Feb 23, 5:00PM, Saturday
|
54
|
+
@h.is_open?(t).must_equal false
|
55
|
+
|
56
|
+
t = Time.new(2013, 2, 21, 8, 0) #2013 Feb 21, 8:00AM Thursday
|
57
|
+
@h.is_open?(t).must_equal true
|
58
|
+
|
59
|
+
t = Time.new(2013, 2, 20, 16, 59) #2013 Feb 20, 4:59PM Wednesday
|
60
|
+
@h.is_open?(t).must_equal true
|
61
|
+
|
62
|
+
t = Time.new(2013, 2, 20, 17, 1) #2013 Feb 20, 5:01PM Wednesday
|
63
|
+
@h.is_open?(t).must_equal false
|
64
|
+
|
65
|
+
t = Time.new(2013, 2, 19, 7, 59) #2013 Feb 19, 7:59AM Tuesday
|
66
|
+
@h.is_open?(t).must_equal false
|
67
|
+
|
68
|
+
t = Time.new(2013, 2, 18, 12, 30) #2013 Feb 18, 12:30PM Monday
|
69
|
+
@h.is_open?(t).must_equal false
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should work with stores what open after the midnight" do
|
73
|
+
text = "Mon-Sun: 12:30AM-10PM"
|
74
|
+
|
75
|
+
r, msg = @h.from_text(text)
|
76
|
+
|
77
|
+
r.must_equal true
|
78
|
+
|
79
|
+
t = Time.new(2013, 2, 24, 0, 0) #2013 Feb 24 12:00AM
|
80
|
+
@h.is_open?(t).must_equal false
|
81
|
+
t = Time.new(2013, 2, 24, 0, 15) # 2013 Feb 24, 12:15AM
|
82
|
+
@h.is_open?(t).must_equal false
|
83
|
+
t = Time.new(2013, 2, 24, 0, 30) #2013 Feb 24, 12:30AM
|
84
|
+
@h.is_open?(t).must_equal true
|
85
|
+
t = Time.new(2013, 2, 24, 22, 0) #2013 Feb 24, 10:00PM
|
86
|
+
@h.is_open?(t).must_equal true
|
87
|
+
t = Time.new(2013, 2, 24, 22, 1) #2013 Feb 24, 10:01PM
|
88
|
+
@h.is_open?(t).must_equal false
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should return false for invalid time periods" do
|
92
|
+
texts = ["Mon-Fri: 9pm - 5am", "Sat: 5:30pm - 1:00pm"]
|
93
|
+
|
94
|
+
texts.each do |t|
|
95
|
+
r, msg = @h.from_text(t)
|
96
|
+
|
97
|
+
r.must_equal false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should return false for invalid day periods" do
|
102
|
+
texts = ["Fri-Mon: 10:00pm - 11pm", "sun-mon: 1am-3pm"]
|
103
|
+
texts.each do |t|
|
104
|
+
r, msg = @h.from_text(t)
|
105
|
+
r.must_equal false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should return false for overlap in time periods for a single day range" do
|
110
|
+
texts = ["mon-fri: 10am-5pm 4pm-10pm", "mon: 10am-5pm 5pm-9pm"]
|
111
|
+
texts.each do |t|
|
112
|
+
r, msg = @h.from_text(t)
|
113
|
+
r.must_equal false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should return false for overlap in the day ranges" do
|
118
|
+
texts = ["mon-fri: 10am-5pm, mon: 6pm-10pm", "mon-fri: 10am-5pm wed:6pm-10pm", "mon-fri: 9am-9pm fri:10pm-11pm"]
|
119
|
+
|
120
|
+
texts.each do |t|
|
121
|
+
r, msg = @h.from_text(t)
|
122
|
+
|
123
|
+
r.must_equal false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should take a block to customize to_text format" do
|
128
|
+
text = "mon-fri: 10am-6pm, 7pm-10pm sat: 1pm-5pm sun: closed"
|
129
|
+
|
130
|
+
r, msg = @h.from_text(text)
|
131
|
+
r.must_equal true
|
132
|
+
|
133
|
+
formatted_text = @h.to_text do |d1, d2, list_of_durations|
|
134
|
+
s = '<tr>'
|
135
|
+
|
136
|
+
s += "<td>#{d1.to_s}"
|
137
|
+
if d2 != d1
|
138
|
+
s += " - #{d2.to_s}"
|
139
|
+
end
|
140
|
+
s += ":"
|
141
|
+
s += "</td>"
|
142
|
+
|
143
|
+
s += "<td>"
|
144
|
+
list_of_durations.each_with_index do |r, index|
|
145
|
+
if index > 0
|
146
|
+
s += " "
|
147
|
+
end
|
148
|
+
s += StoreHours::from_minutes_to_time_str(r.first)
|
149
|
+
s += "-"
|
150
|
+
s += StoreHours::from_minutes_to_time_str(r.last)
|
151
|
+
end
|
152
|
+
s += "</td>"
|
153
|
+
|
154
|
+
s += "</tr>"
|
155
|
+
end
|
156
|
+
|
157
|
+
formatted_text.must_equal "<tr><td>Mon - Fri:</td><td>10:00AM-6:00PM 7:00PM-10:00PM</td></tr><tr><td>Sat:</td><td>1:00PM-5:00PM</td></tr><tr><td>Sun:</td><td>-1:59AM--1:59AM</td></tr>"
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: store_hours
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yanhao Zhu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-06-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: parslet
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.5.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.5.0
|
27
|
+
description: A small parser for store normal business hours
|
28
|
+
email:
|
29
|
+
- yanhaozhu@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- .gitignore
|
35
|
+
- Gemfile
|
36
|
+
- LICENSE.txt
|
37
|
+
- README.md
|
38
|
+
- Rakefile
|
39
|
+
- lib/store_hours.rb
|
40
|
+
- lib/store_hours/common_methods.rb
|
41
|
+
- lib/store_hours/constants.rb
|
42
|
+
- lib/store_hours/semantic_error.rb
|
43
|
+
- lib/store_hours/text_input_parser.rb
|
44
|
+
- lib/store_hours/tree_transformer.rb
|
45
|
+
- lib/store_hours/version.rb
|
46
|
+
- store_hours.gemspec
|
47
|
+
- test/store_hours_test.rb
|
48
|
+
homepage: https://github.com/luanzhu/store_hours
|
49
|
+
licenses: []
|
50
|
+
metadata: {}
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 2.0.3
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: A small parser for store normal business hours
|
71
|
+
test_files:
|
72
|
+
- test/store_hours_test.rb
|