store_hours 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in store_hours.gemspec
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'rake', '~> 0.9.2.2'
8
+ end
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
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'lib'
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose = true
8
+ end
@@ -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,6 @@
1
+ module StoreHours
2
+
3
+ WEEKDAY_TO_NUM = { :mon => 1, :tue => 2, :wed => 3, :thu => 4, :fri => 5, :sat => 6, :sun => 7}
4
+
5
+ NUM_TO_WEEKDAY = { 1=> :Mon, 2 => :Tue, 3 => :Wed, 4 => :Thu, 5 => :Fri, 6 => :Sat, 7 => :Sun }
6
+ 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
@@ -0,0 +1,3 @@
1
+ module StoreHours
2
+ VERSION = "0.0.2"
3
+ end
@@ -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