store_hours 0.0.2
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 +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
|