emery 0.0.1
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 +7 -0
- data/lib/emery.rb +6 -0
- data/lib/emery/dataclass.rb +86 -0
- data/lib/emery/enum.rb +101 -0
- data/lib/emery/jsoner.rb +188 -0
- data/lib/emery/tod.rb +262 -0
- data/lib/emery/type.rb +176 -0
- data/test/dataclass_test.rb +115 -0
- data/test/enum_test.rb +47 -0
- data/test/jsoner_test.rb +239 -0
- data/test/tod_test.rb +41 -0
- data/test/type_test.rb +185 -0
- metadata +67 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8dc449ad3ba135140479a6711b016e63bfc30f9fd6dcf6dba8ff78ee71208a6a
|
4
|
+
data.tar.gz: 9c2f86bde90521cfa5adb78eda8ceef5504c2fbcf44de558cf2450c550cea96e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e9695fe9eeed44ad9d3bc33e1dc92ae4de5a31e024c7fa9ba1fa34dbd3df554cf5cd73e9634dbaec1246ca80b8ac536fd1020b129ef26d9973512c34ca77b920
|
7
|
+
data.tar.gz: 8f7b8961c1c39cf2a354163725bd1fc761e620627ebbb73cb1dd91c7d5746bdc6fd937ccc17fa2bf70ef7af826dd0eb60f09102a4b27ab7f6cf1343633686ecf
|
data/lib/emery.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Emery
|
2
|
+
module DataClass
|
3
|
+
def initialize(params)
|
4
|
+
self.class.json_attributes.each do |attr, attr_type|
|
5
|
+
attr_value = params[attr]
|
6
|
+
self.instance_variable_set("@#{attr}", T.check_var(attr, attr_type, attr_value))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
begin
|
12
|
+
T.check(self.class, other)
|
13
|
+
self.class.json_attributes.keys.each do |attr|
|
14
|
+
if self.instance_variable_get("@#{attr}") != other.instance_variable_get("@#{attr}")
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
return true
|
19
|
+
rescue
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def copy(params)
|
25
|
+
params.each do |attr, attr_value|
|
26
|
+
if !self.class.json_attributes.key?(attr)
|
27
|
+
raise TypeError.new("Non existing attribute #{attr}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
new_params =
|
31
|
+
self.class.json_attributes.map do |attr, attr_type|
|
32
|
+
attr_value =
|
33
|
+
if params.key?(attr)
|
34
|
+
params[attr]
|
35
|
+
else
|
36
|
+
self.instance_variable_get("@#{attr}")
|
37
|
+
end
|
38
|
+
[attr, attr_value]
|
39
|
+
end.to_h
|
40
|
+
return self.class.new(new_params)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.included(base)
|
44
|
+
base.extend ClassMethods
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def json_attributes
|
49
|
+
@json_attributes
|
50
|
+
end
|
51
|
+
|
52
|
+
def val(name, type)
|
53
|
+
if @json_attributes == nil
|
54
|
+
@json_attributes = {}
|
55
|
+
end
|
56
|
+
@json_attributes[name] = type
|
57
|
+
attr_reader name
|
58
|
+
end
|
59
|
+
|
60
|
+
def var(name, type)
|
61
|
+
if @json_attributes == nil
|
62
|
+
@json_attributes = {}
|
63
|
+
end
|
64
|
+
@json_attributes[name] = type
|
65
|
+
attr_accessor name
|
66
|
+
end
|
67
|
+
|
68
|
+
def jsoner_deserialize(json_value)
|
69
|
+
T.check(T.hash(String, NilableUntyped), json_value)
|
70
|
+
parameters = @json_attributes.map do |attr, attr_type|
|
71
|
+
attr_value = json_value[attr.to_s]
|
72
|
+
[attr, Jsoner.deserialize(attr_type, attr_value)]
|
73
|
+
end
|
74
|
+
return self.new parameters.to_h
|
75
|
+
end
|
76
|
+
|
77
|
+
def jsoner_serialize(value)
|
78
|
+
T.check(self, value)
|
79
|
+
attrs = @json_attributes.map do |attr, attr_type|
|
80
|
+
[attr, Jsoner.serialize(attr_type, value.send(attr))]
|
81
|
+
end
|
82
|
+
return attrs.to_h
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/emery/enum.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module Emery
|
2
|
+
module Enum
|
3
|
+
attr_reader :key, :value
|
4
|
+
|
5
|
+
def initialize(key, value)
|
6
|
+
@key = key
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.extend Enumerable
|
12
|
+
base.extend ClassMethods
|
13
|
+
|
14
|
+
base.private_class_method(:new)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def check(value)
|
19
|
+
T.check_not_nil(self, value)
|
20
|
+
if !value?(value)
|
21
|
+
raise TypeError.new("Value '#{value.inspect.to_s}' is not a member of enum #{self}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def jsoner_deserialize(json_value)
|
26
|
+
T.check(self, json_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def jsoner_serialize(value)
|
30
|
+
T.check(self, value)
|
31
|
+
end
|
32
|
+
|
33
|
+
def define(key, value)
|
34
|
+
@_enum_hash ||= {}
|
35
|
+
@_enums_by_value ||= {}
|
36
|
+
|
37
|
+
if @_enum_hash.key?(key) then
|
38
|
+
raise TypeError.new("Duplicate key: #{key}")
|
39
|
+
end
|
40
|
+
|
41
|
+
if @_enums_by_value.key?(value) then
|
42
|
+
raise TypeError.new("Duplicate value: #{value}")
|
43
|
+
end
|
44
|
+
|
45
|
+
new_instance = new(key, value)
|
46
|
+
@_enum_hash[key] = new_instance
|
47
|
+
@_enums_by_value[value] = new_instance
|
48
|
+
|
49
|
+
if key.to_s == key.to_s.upcase
|
50
|
+
const_set key, value
|
51
|
+
else
|
52
|
+
define_singleton_method(key) { value }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def each(&block)
|
57
|
+
@_enum_hash.each(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse(k)
|
61
|
+
k = k.to_s.upcase
|
62
|
+
each do |key, enum|
|
63
|
+
return enum.value if key.to_s.upcase == k
|
64
|
+
end
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def key?(k)
|
69
|
+
@_enum_hash.key?(k)
|
70
|
+
end
|
71
|
+
|
72
|
+
def value(k)
|
73
|
+
enum = @_enum_hash[k]
|
74
|
+
enum.value if enum
|
75
|
+
end
|
76
|
+
|
77
|
+
def value?(v)
|
78
|
+
@_enums_by_value.key?(v)
|
79
|
+
end
|
80
|
+
|
81
|
+
def key(v)
|
82
|
+
enum = @_enums_by_value[v]
|
83
|
+
enum.key if enum
|
84
|
+
end
|
85
|
+
|
86
|
+
def keys
|
87
|
+
@_enum_hash.values.map(&:key)
|
88
|
+
end
|
89
|
+
|
90
|
+
def values
|
91
|
+
@_enum_hash.values.map(&:value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_h
|
95
|
+
Hash[@_enum_hash.map do |key, enum|
|
96
|
+
[key, enum.value]
|
97
|
+
end]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/emery/jsoner.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
require "json"
|
2
|
+
require "date"
|
3
|
+
|
4
|
+
require "emery/type"
|
5
|
+
|
6
|
+
module Emery
|
7
|
+
class JsonerError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
module Jsoner
|
11
|
+
T::StringFormatted.class_eval do
|
12
|
+
def jsoner_deserialize(json_value)
|
13
|
+
T.check(self, json_value)
|
14
|
+
end
|
15
|
+
def jsoner_serialize(value)
|
16
|
+
T.check(self, value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
T::ArrayType.class_eval do
|
21
|
+
def jsoner_deserialize(json_value)
|
22
|
+
T.check_not_nil(self, json_value)
|
23
|
+
if !json_value.is_a?(Array)
|
24
|
+
raise JsonerError.new("JSON value type #{json_value.class} is not Array")
|
25
|
+
end
|
26
|
+
json_value.map { |item_json_value| Jsoner.deserialize(self.item_type, item_json_value) }
|
27
|
+
end
|
28
|
+
def jsoner_serialize(value)
|
29
|
+
if !value.is_a?(Array)
|
30
|
+
raise JsonerError.new("Value type #{json_value.class} is not Array")
|
31
|
+
end
|
32
|
+
value.map { |item| Jsoner.serialize(self.item_type, item) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
T::HashType.class_eval do
|
37
|
+
def jsoner_deserialize(json_value)
|
38
|
+
T.check_not_nil(self, json_value)
|
39
|
+
if self.key_type != String
|
40
|
+
raise JsonerError.new("Hash key type #{self.key_type} is not supported for JSON (de)serialization - key should be String")
|
41
|
+
end
|
42
|
+
if !json_value.is_a?(Hash)
|
43
|
+
raise JsonerError.new("JSON value type #{json_value.class} is not Hash")
|
44
|
+
end
|
45
|
+
json_value.map do |key, value|
|
46
|
+
[T.check(self.key_type, key), Jsoner.deserialize(self.value_type, value)]
|
47
|
+
end.to_h
|
48
|
+
end
|
49
|
+
def jsoner_serialize(value)
|
50
|
+
if self.key_type != String
|
51
|
+
raise JsonerError.new("Hash key type #{self.key_type} is not supported for JSON (de)serialization - key should be String")
|
52
|
+
end
|
53
|
+
if !value.is_a?(Hash)
|
54
|
+
raise JsonerError.new("Value type #{value.class} is not Hash")
|
55
|
+
end
|
56
|
+
value.map do |key, value|
|
57
|
+
[T.check(self.key_type, key), Jsoner.serialize(self.value_type, value)]
|
58
|
+
end.to_h
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
T::AnyType.class_eval do
|
63
|
+
def jsoner_deserialize(json_value)
|
64
|
+
types.each do |type|
|
65
|
+
begin
|
66
|
+
return Jsoner.deserialize(type, json_value)
|
67
|
+
rescue TypeError
|
68
|
+
end
|
69
|
+
end
|
70
|
+
raise JsonerError.new("Value '#{json_value.inspect.to_s}' can not be deserialized as any of #{@types.map { |t| t.to_s}.join(', ')}")
|
71
|
+
end
|
72
|
+
def jsoner_serialize(value)
|
73
|
+
T.check(self, value)
|
74
|
+
type = types.find {|t| T.instance_of?(t, value) }
|
75
|
+
Jsoner.serialize(type, value)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
T::Nilable.class_eval do
|
80
|
+
def jsoner_deserialize(json_value)
|
81
|
+
if json_value != nil
|
82
|
+
Jsoner.deserialize(self.type, json_value)
|
83
|
+
else
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
def jsoner_serialize(value)
|
88
|
+
if value != nil
|
89
|
+
Jsoner.serialize(self.type, value)
|
90
|
+
else
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module FloatSerializer
|
97
|
+
def self.jsoner_deserialize(json_value)
|
98
|
+
T.check(T.any(Float, Integer), json_value)
|
99
|
+
json_value.to_f
|
100
|
+
end
|
101
|
+
def self.jsoner_serialize(value)
|
102
|
+
T.check(Float, value)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
module DateTimeSerializer
|
107
|
+
def self.jsoner_deserialize(json_value)
|
108
|
+
T.check(String, json_value)
|
109
|
+
begin
|
110
|
+
DateTime.strptime(json_value, '%Y-%m-%dT%H:%M:%S')
|
111
|
+
rescue
|
112
|
+
raise JsonerError.new("Failed to parse DateTime from '#{json_value.inspect.to_s}' format %Y-%m-%dT%H:%M:%S is required")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
def self.jsoner_serialize(value)
|
116
|
+
T.check(DateTime, value)
|
117
|
+
value.strftime('%Y-%m-%dT%H:%M:%S')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
module DateSerializer
|
122
|
+
def self.jsoner_deserialize(json_value)
|
123
|
+
T.check(String, json_value)
|
124
|
+
begin
|
125
|
+
Date.strptime(json_value, '%Y-%m-%d')
|
126
|
+
rescue
|
127
|
+
raise JsonerError.new("Failed to parse Date from '#{json_value.inspect.to_s}' format %Y-%m-%d is required")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
def self.jsoner_serialize(value)
|
131
|
+
T.check(Date, value)
|
132
|
+
value.strftime('%Y-%m-%d')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
@@serializers = {
|
137
|
+
Float => FloatSerializer,
|
138
|
+
Date => DateSerializer,
|
139
|
+
DateTime => DateTimeSerializer
|
140
|
+
}
|
141
|
+
def self.add_serializer(type, serializer)
|
142
|
+
@@serializers[type] = serializer
|
143
|
+
end
|
144
|
+
|
145
|
+
def Jsoner.from_json(type, json)
|
146
|
+
data = JSON.parse(json)
|
147
|
+
return deserialize(type, data)
|
148
|
+
end
|
149
|
+
|
150
|
+
def Jsoner.deserialize(type, json_value)
|
151
|
+
begin
|
152
|
+
if type.methods.include? :jsoner_deserialize
|
153
|
+
return type.jsoner_deserialize(json_value)
|
154
|
+
elsif @@serializers.include? type
|
155
|
+
return @@serializers[type].jsoner_deserialize(json_value)
|
156
|
+
else
|
157
|
+
if ![String, Float, Integer, TrueClass, FalseClass, NilClass].include? type
|
158
|
+
raise JsonerError.new("Type #{type} is not supported in Jsoner deserialization")
|
159
|
+
end
|
160
|
+
return T.check(type, json_value)
|
161
|
+
end
|
162
|
+
rescue StandardError => error
|
163
|
+
raise JsonerError.new(error.message)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def Jsoner.to_json(type, value)
|
168
|
+
JSON.dump(serialize(type, value))
|
169
|
+
end
|
170
|
+
|
171
|
+
def Jsoner.serialize(type, value)
|
172
|
+
begin
|
173
|
+
if type.methods.include? :jsoner_serialize
|
174
|
+
return type.jsoner_serialize(value)
|
175
|
+
elsif @@serializers.include? type
|
176
|
+
return @@serializers[type].jsoner_serialize(value)
|
177
|
+
else
|
178
|
+
if ![String, Float, Integer, TrueClass, FalseClass, NilClass].include? type
|
179
|
+
raise JsonerError.new("Type #{type} is not supported in Jsoner serialization")
|
180
|
+
end
|
181
|
+
return T.check(type, value)
|
182
|
+
end
|
183
|
+
rescue StandardError => error
|
184
|
+
raise JsonerError.new(error.message)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/emery/tod.rb
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
source: https://github.com/jackc/tod
|
4
|
+
|
5
|
+
Copyright (c) 2010-2015 Jack Christensen
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
a copy of this software and associated documentation files (the
|
9
|
+
"Software"), to deal in the Software without restriction, including
|
10
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be
|
16
|
+
included in all copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
=end
|
27
|
+
|
28
|
+
module Emery
|
29
|
+
class TimeOfDay
|
30
|
+
include Comparable
|
31
|
+
|
32
|
+
def self.jsoner_deserialize(json_value)
|
33
|
+
TimeOfDay.parse(T.check(String, json_value))
|
34
|
+
end
|
35
|
+
def self.jsoner_serialize(value)
|
36
|
+
T.check(TimeOfDay, value).to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :hour, :minute, :second, :second_of_day
|
40
|
+
alias_method :min, :minute
|
41
|
+
alias_method :sec, :second
|
42
|
+
alias_method :to_i, :second_of_day
|
43
|
+
|
44
|
+
PARSE_24H_REGEX = /
|
45
|
+
\A
|
46
|
+
([01]?\d|2[0-4])
|
47
|
+
:?
|
48
|
+
([0-5]\d)?
|
49
|
+
:?
|
50
|
+
([0-5]\d)?
|
51
|
+
\z
|
52
|
+
/x
|
53
|
+
|
54
|
+
PARSE_12H_REGEX = /
|
55
|
+
\A
|
56
|
+
(0?\d|1[0-2])
|
57
|
+
:?
|
58
|
+
([0-5]\d)?
|
59
|
+
:?
|
60
|
+
([0-5]\d)?
|
61
|
+
\s*
|
62
|
+
([ap])
|
63
|
+
\.?
|
64
|
+
\s*
|
65
|
+
m?
|
66
|
+
\.?
|
67
|
+
\z
|
68
|
+
/x
|
69
|
+
|
70
|
+
WORDS = {
|
71
|
+
"noon" => "12pm".freeze,
|
72
|
+
"midnight" => "12am".freeze
|
73
|
+
}
|
74
|
+
|
75
|
+
NUM_SECONDS_IN_DAY = 86400
|
76
|
+
NUM_SECONDS_IN_HOUR = 3600
|
77
|
+
NUM_SECONDS_IN_MINUTE = 60
|
78
|
+
|
79
|
+
FORMATS = {
|
80
|
+
short: "%-l:%M %P".freeze,
|
81
|
+
medium: "%-l:%M:%S %P".freeze,
|
82
|
+
time: "%H:%M".freeze
|
83
|
+
}
|
84
|
+
|
85
|
+
def initialize(h, m=0, s=0)
|
86
|
+
@hour = Integer(h)
|
87
|
+
@minute = Integer(m)
|
88
|
+
@second = Integer(s)
|
89
|
+
|
90
|
+
raise ArgumentError, "hour must be between 0 and 24" unless (0..24).include?(@hour)
|
91
|
+
if @hour == 24 && (@minute != 0 || @second != 0)
|
92
|
+
raise ArgumentError, "hour can only be 24 when minute and second are 0"
|
93
|
+
end
|
94
|
+
raise ArgumentError, "minute must be between 0 and 59" unless (0..59).include?(@minute)
|
95
|
+
raise ArgumentError, "second must be between 0 and 59" unless (0..59).include?(@second)
|
96
|
+
|
97
|
+
@second_of_day = @hour * 60 * 60 + @minute * 60 + @second
|
98
|
+
|
99
|
+
freeze # TimeOfDay instances are value objects
|
100
|
+
end
|
101
|
+
|
102
|
+
def <=>(other)
|
103
|
+
return unless other.respond_to?(:second_of_day)
|
104
|
+
@second_of_day <=> other.second_of_day
|
105
|
+
end
|
106
|
+
|
107
|
+
# Rounding to the given nearest number of seconds
|
108
|
+
def round(round_sec = 1)
|
109
|
+
down = self - (self.to_i % round_sec)
|
110
|
+
up = down + round_sec
|
111
|
+
|
112
|
+
difference_down = self - down
|
113
|
+
difference_up = up - self
|
114
|
+
|
115
|
+
if (difference_down < difference_up)
|
116
|
+
return down
|
117
|
+
else
|
118
|
+
return up
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Formats identically to Time#strftime
|
123
|
+
def strftime(format_string)
|
124
|
+
# Special case 2400 because strftime will load TimeOfDay into Time which
|
125
|
+
# will convert 24 to 0
|
126
|
+
format_string = format_string.gsub(/%H|%k/, '24') if @hour == 24
|
127
|
+
Time.local(2000,1,1, @hour, @minute, @second).strftime(format_string)
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_formatted_s(format = :default)
|
131
|
+
if formatter = FORMATS[format]
|
132
|
+
if formatter.respond_to?(:call)
|
133
|
+
formatter.call(self).to_s
|
134
|
+
else
|
135
|
+
strftime(formatter)
|
136
|
+
end
|
137
|
+
else
|
138
|
+
strftime "%H:%M:%S"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
alias_method :to_s, :to_formatted_s
|
142
|
+
|
143
|
+
def value_for_database
|
144
|
+
to_s
|
145
|
+
end
|
146
|
+
|
147
|
+
# Return a new TimeOfDay num_seconds greater than self. It will wrap around
|
148
|
+
# at midnight.
|
149
|
+
def +(num_seconds)
|
150
|
+
TimeOfDay.from_second_of_day @second_of_day + num_seconds
|
151
|
+
end
|
152
|
+
|
153
|
+
# Return a new TimeOfDay num_seconds less than self. It will wrap around
|
154
|
+
# at midnight.
|
155
|
+
def -(other)
|
156
|
+
if other.instance_of?(TimeOfDay)
|
157
|
+
TimeOfDay.from_second_of_day @second_of_day - other.second_of_day
|
158
|
+
else
|
159
|
+
TimeOfDay.from_second_of_day @second_of_day - other
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns a Time instance on date using self as the time of day
|
164
|
+
# Optional time_zone will build time in that zone
|
165
|
+
def on(date, time_zone=Tod::TimeOfDay.time_zone)
|
166
|
+
time_zone.local date.year, date.month, date.day, @hour, @minute, @second
|
167
|
+
end
|
168
|
+
|
169
|
+
# Build a new TimeOfDay instance from second_of_day
|
170
|
+
#
|
171
|
+
# TimeOfDay.from_second_of_day(3600) == TimeOfDay.new(1) # => true
|
172
|
+
def self.from_second_of_day(second_of_day)
|
173
|
+
second_of_day = Integer(second_of_day)
|
174
|
+
return new 24 if second_of_day == NUM_SECONDS_IN_DAY
|
175
|
+
remaining_seconds = second_of_day % NUM_SECONDS_IN_DAY
|
176
|
+
hour = remaining_seconds / NUM_SECONDS_IN_HOUR
|
177
|
+
remaining_seconds -= hour * NUM_SECONDS_IN_HOUR
|
178
|
+
minute = remaining_seconds / NUM_SECONDS_IN_MINUTE
|
179
|
+
remaining_seconds -= minute * NUM_SECONDS_IN_MINUTE
|
180
|
+
new hour, minute, remaining_seconds
|
181
|
+
end
|
182
|
+
class << self
|
183
|
+
alias :from_i :from_second_of_day
|
184
|
+
end
|
185
|
+
|
186
|
+
# Build a TimeOfDay instance from string
|
187
|
+
#
|
188
|
+
# Strings only need to contain an hour. Minutes, seconds, AM or PM, and colons
|
189
|
+
# are all optional.
|
190
|
+
# TimeOfDay.parse "8" # => 08:00:00
|
191
|
+
# TimeOfDay.parse "8am" # => 08:00:00
|
192
|
+
# TimeOfDay.parse "8pm" # => 20:00:00
|
193
|
+
# TimeOfDay.parse "8p" # => 20:00:00
|
194
|
+
# TimeOfDay.parse "9:30" # => 09:30:00
|
195
|
+
# TimeOfDay.parse "15:30" # => 15:30:00
|
196
|
+
# TimeOfDay.parse "3:30pm" # => 15:30:00
|
197
|
+
# TimeOfDay.parse "1230" # => 12:30:00
|
198
|
+
# TimeOfDay.parse "3:25:58" # => 03:25:58
|
199
|
+
# TimeOfDay.parse "515p" # => 17:15:00
|
200
|
+
# TimeOfDay.parse "151253" # => 15:12:53
|
201
|
+
# You can give a block, that is called with the input if the string is not parsable.
|
202
|
+
# If no block is given an ArgumentError is raised if try_parse returns nil.
|
203
|
+
def self.parse(tod_string)
|
204
|
+
try_parse(tod_string) || (block_given? ? yield(tod_string) : raise(ArgumentError, "Invalid time of day string"))
|
205
|
+
end
|
206
|
+
|
207
|
+
# Same as parse(), but return nil if not parsable (instead of raising an error)
|
208
|
+
# TimeOfDay.try_parse "8am" # => 08:00:00
|
209
|
+
# TimeOfDay.try_parse "" # => nil
|
210
|
+
# TimeOfDay.try_parse "abc" # => nil
|
211
|
+
def self.try_parse(tod_string)
|
212
|
+
tod_string = tod_string.to_s
|
213
|
+
tod_string = tod_string.strip
|
214
|
+
tod_string = tod_string.downcase
|
215
|
+
tod_string = WORDS[tod_string] || tod_string
|
216
|
+
if PARSE_24H_REGEX =~ tod_string || PARSE_12H_REGEX =~ tod_string
|
217
|
+
hour, minute, second, a_or_p = $1.to_i, $2.to_i, $3.to_i, $4
|
218
|
+
if hour == 12 && a_or_p == "a"
|
219
|
+
hour = 0
|
220
|
+
elsif hour < 12 && a_or_p == "p"
|
221
|
+
hour += 12
|
222
|
+
end
|
223
|
+
|
224
|
+
new hour, minute, second
|
225
|
+
else
|
226
|
+
nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Determine if a string is parsable into a TimeOfDay instance
|
231
|
+
# TimeOfDay.parsable? "8am" # => true
|
232
|
+
# TimeOfDay.parsable? "abc" # => false
|
233
|
+
def self.parsable?(tod_string)
|
234
|
+
!!try_parse(tod_string)
|
235
|
+
end
|
236
|
+
|
237
|
+
# If ActiveSupport TimeZone is available and set use current time zone else return Time
|
238
|
+
def self.time_zone
|
239
|
+
(Time.respond_to?(:zone) && Time.zone) || Time
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.dump(time_of_day)
|
243
|
+
time_of_day =
|
244
|
+
if time_of_day.is_a? Hash
|
245
|
+
# rails multiparam attribute
|
246
|
+
# get hour, minute and second and construct new TimeOfDay object
|
247
|
+
::Tod::TimeOfDay.new(time_of_day[4], time_of_day[5], time_of_day[6])
|
248
|
+
else
|
249
|
+
# return nil, if input is not parsable
|
250
|
+
Tod::TimeOfDay(time_of_day){}
|
251
|
+
end
|
252
|
+
time_of_day.to_s if time_of_day
|
253
|
+
end
|
254
|
+
|
255
|
+
def self.load(time)
|
256
|
+
if time && !time.to_s.empty?
|
257
|
+
return ::Tod::TimeOfDay.new(24) if time.respond_to?(:day) && time.day == 2 && time.hour == 0 && time.min == 0 && time.sec == 0
|
258
|
+
::Tod::TimeOfDay(time)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|