momomoto 0.1.13 → 0.1.14
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.
- data/Rakefile +15 -0
- data/lib/momomoto/base.rb +77 -12
- data/lib/momomoto/database.rb +16 -4
- data/lib/momomoto/datatype/base.rb +49 -8
- data/lib/momomoto/datatype/bigint.rb +2 -1
- data/lib/momomoto/datatype/boolean.rb +9 -0
- data/lib/momomoto/datatype/bytea.rb +4 -0
- data/lib/momomoto/datatype/character.rb +2 -0
- data/lib/momomoto/datatype/character_varying.rb +2 -0
- data/lib/momomoto/datatype/date.rb +5 -0
- data/lib/momomoto/datatype/inet.rb +4 -0
- data/lib/momomoto/datatype/integer.rb +5 -0
- data/lib/momomoto/datatype/interval.rb +14 -1
- data/lib/momomoto/datatype/numeric.rb +5 -0
- data/lib/momomoto/datatype/real.rb +2 -0
- data/lib/momomoto/datatype/smallint.rb +2 -0
- data/lib/momomoto/datatype/text.rb +14 -0
- data/lib/momomoto/datatype/time_with_time_zone.rb +3 -1
- data/lib/momomoto/datatype/time_without_time_zone.rb +10 -0
- data/lib/momomoto/datatype/timestamp_with_time_zone.rb +2 -0
- data/lib/momomoto/datatype/timestamp_without_time_zone.rb +5 -0
- data/lib/momomoto/information_schema/columns.rb +8 -0
- data/lib/momomoto/information_schema/fetch_procedure_columns.rb +2 -0
- data/lib/momomoto/information_schema/fetch_procedure_parameters.rb +2 -0
- data/lib/momomoto/information_schema/key_column_usage.rb +5 -0
- data/lib/momomoto/information_schema/routines.rb +4 -0
- data/lib/momomoto/information_schema/table_constraints.rb +5 -0
- data/lib/momomoto/order.rb +61 -0
- data/lib/momomoto/procedure.rb +14 -14
- data/lib/momomoto/row.rb +41 -4
- data/lib/momomoto/table.rb +106 -20
- data/lib/timeinterval.rb +90 -16
- data/test/test_datatype.rb +1 -1
- metadata +2 -2
data/lib/momomoto/row.rb
CHANGED
@@ -1,62 +1,86 @@
|
|
1
1
|
|
2
2
|
module Momomoto
|
3
|
-
|
3
|
+
|
4
|
+
# Base class for all Rows.
|
4
5
|
class Row
|
5
6
|
|
6
|
-
#
|
7
|
+
# undefining fields to avoid conflicts
|
7
8
|
undef :id,:type
|
8
9
|
|
10
|
+
# Getter for the table this Row is using.
|
9
11
|
def self.table
|
10
12
|
@table
|
11
13
|
end
|
12
14
|
|
15
|
+
# Getter for the columns of this Row's table.
|
13
16
|
def self.columns
|
14
17
|
@columns
|
15
18
|
end
|
16
19
|
|
20
|
+
# Getter for the order of columns. The order is built from
|
21
|
+
# +columns+.keys.
|
17
22
|
def self.column_order
|
18
23
|
@column_order
|
19
24
|
end
|
20
25
|
|
26
|
+
# Gets the value of a given +fieldname+.
|
27
|
+
#
|
28
|
+
# feed = Feeds.select( {:url => 'https://www.c3d2.de/news-atom.xml' )[0]
|
29
|
+
# feed[:url] == 'https://www.c3d2.de/news-atom.xml'
|
30
|
+
# => true
|
31
|
+
#
|
21
32
|
def []( fieldname )
|
22
33
|
get_column( fieldname )
|
23
34
|
end
|
24
35
|
|
36
|
+
# Sets +fieldname+ to +value+.
|
37
|
+
#
|
38
|
+
# feed = Feeds.select( {:url => 'http://www.c3d2.de/news-atom.xml' )[0]
|
39
|
+
# feed[:url_host] = 'https://www.c3d2.de/'
|
25
40
|
def []=( fieldname, value )
|
26
41
|
set_column( fieldname, value )
|
27
42
|
end
|
28
43
|
|
44
|
+
# Compares +@data+ value of the row with +other+.
|
29
45
|
def ==( other )
|
30
46
|
@data == other.instance_variable_get( :@data )
|
31
47
|
end
|
32
48
|
|
49
|
+
# Getter for +@dirty+ which holds all changed fields of a row.
|
33
50
|
def dirty
|
34
51
|
@dirty
|
35
52
|
end
|
36
53
|
|
54
|
+
# Returns true if there are fields in +@dirty+.
|
37
55
|
def dirty?
|
38
56
|
@dirty.length > 0
|
39
57
|
end
|
40
58
|
|
59
|
+
# Marks a field as dirty.
|
41
60
|
def mark_dirty( field )
|
42
61
|
field = field.to_sym
|
43
62
|
@dirty.push( field ) if not @dirty.member?( field )
|
44
63
|
end
|
45
64
|
|
65
|
+
# Removes all fields from +dirty+.
|
46
66
|
def clean_dirty
|
47
67
|
@dirty = []
|
48
68
|
end
|
49
69
|
|
70
|
+
# Creates a new Row instance.
|
50
71
|
def initialize( data = [] )
|
51
72
|
@data = data
|
52
73
|
@new_record = false
|
53
74
|
clean_dirty
|
54
75
|
end
|
55
76
|
|
77
|
+
# Returns true if the row is newly created.
|
56
78
|
def new_record?
|
57
79
|
@new_record
|
58
80
|
end
|
59
81
|
|
82
|
+
# Sets +@new_record+ to +true+
|
83
|
+
# or +false+.
|
60
84
|
def new_record=( value )
|
61
85
|
@new_record = !!value
|
62
86
|
end
|
@@ -71,7 +95,7 @@ module Momomoto
|
|
71
95
|
self.class.table.delete( self )
|
72
96
|
end
|
73
97
|
|
74
|
-
#
|
98
|
+
# converts row to hash
|
75
99
|
def to_hash
|
76
100
|
hash = {}
|
77
101
|
self.class.columns.keys.each do | key |
|
@@ -80,7 +104,20 @@ module Momomoto
|
|
80
104
|
hash
|
81
105
|
end
|
82
106
|
|
83
|
-
#
|
107
|
+
# Generic setter for column values. You should use this method when
|
108
|
+
# you are defining your own setter. This is useful for preprocessing +value+
|
109
|
+
# before it is written to database:
|
110
|
+
#
|
111
|
+
# class Person < Momomoto::Table
|
112
|
+
# module Methods
|
113
|
+
# def nickname=( value )
|
114
|
+
# set_column( :nickname, value.downcase )
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# This defines a custom setter nickname= which invokes downcase
|
120
|
+
# on the given +value+.
|
84
121
|
def set_column( column, value )
|
85
122
|
raise "Unknown column #{column}" if not self.class.column_order.member?( column.to_sym )
|
86
123
|
table = self.class.table
|
data/lib/momomoto/table.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
|
2
2
|
module Momomoto
|
3
3
|
|
4
|
-
#
|
5
|
-
#
|
4
|
+
# This class implements access to tables/views.
|
5
|
+
# It must not be used directly but you should inherit from this class.
|
6
6
|
class Table < Base
|
7
7
|
|
8
8
|
class << self
|
@@ -43,7 +43,7 @@ module Momomoto
|
|
43
43
|
@table_name
|
44
44
|
end
|
45
45
|
|
46
|
-
# get the full name of
|
46
|
+
# get the full name of table including, if set, schema
|
47
47
|
def full_name
|
48
48
|
"#{ schema_name ? schema_name + '.' : ''}#{table_name}"
|
49
49
|
end
|
@@ -62,7 +62,64 @@ module Momomoto
|
|
62
62
|
@primary_keys
|
63
63
|
end
|
64
64
|
|
65
|
-
# Searches for records and returns an
|
65
|
+
# Searches for records and returns an Array containing the records.
|
66
|
+
# There are a bunch of different use cases as this method is the primary way
|
67
|
+
# to access all rows in the database.
|
68
|
+
#
|
69
|
+
# Selecting rows based on expression:
|
70
|
+
# #selects the feeds that match both the given url and author fields
|
71
|
+
# Posts.select(:feed_url => "https://www.c3d2.de/news-atom.xml",:author => "fnord")
|
72
|
+
#
|
73
|
+
# Using order statements:
|
74
|
+
# See Order#asc, Order#desc and Order#lower
|
75
|
+
#
|
76
|
+
# #Selects conferences depending on start_date, starting with the oldest date.
|
77
|
+
# #If two conferences start at the same date(day) use the second order parameter
|
78
|
+
# #start_time.
|
79
|
+
# Conference.select({},{:order => Momomoto.asc([:start_date,:start_time])} )
|
80
|
+
#
|
81
|
+
# Using limit statement:
|
82
|
+
# See Base#compile_limit
|
83
|
+
#
|
84
|
+
# #selects five feeds
|
85
|
+
# five_feeds = Feeds.select( {},{:limit => 5} )
|
86
|
+
#
|
87
|
+
# Using offset statement:
|
88
|
+
# See Base#compile_offset
|
89
|
+
#
|
90
|
+
# #selects five feeds ommitting the first 23 rows
|
91
|
+
# five_feeds = Feeds.select( {}, {:offset => 23, :limit => 5} )
|
92
|
+
#
|
93
|
+
# Using logical operators:
|
94
|
+
# See Datatype::Base#operator_sign for basic comparison operators
|
95
|
+
# See Base#logical_operator for the supported logical operators
|
96
|
+
#
|
97
|
+
# #selects the posts where the content field case-insensitevely matches
|
98
|
+
# #"surveillance".
|
99
|
+
# Posts.select( :content => {:ilike => 'surveillance'} )
|
100
|
+
#
|
101
|
+
# #selects all conferences with a start_date before the current time.
|
102
|
+
# Conferences.select( :start_date => {:le => Time.now} )
|
103
|
+
#
|
104
|
+
# feed1 = "https://www.c3d2.de/news-atom.xml"
|
105
|
+
# feed2 = "http://www.c3d2.de/news-atom.xml"
|
106
|
+
# #selects the feeds with a field url that matches either feed1 or feed2
|
107
|
+
# Feeds.select( :OR=>{:url => [feed1,feed2]} )
|
108
|
+
#
|
109
|
+
#
|
110
|
+
# Selecting only given columns:
|
111
|
+
# See Base#initialize_row for the implementation
|
112
|
+
#
|
113
|
+
# #selects title and content for every row found in table Posts.
|
114
|
+
# posts = Posts.select({},{:columns => [:title,:content]} )
|
115
|
+
#
|
116
|
+
# The returned rows are special. They do not contain getter and setter
|
117
|
+
# for the rest of the columns of the row. Only the specified columns
|
118
|
+
# and all the primary keys of the table have proper accessor methods.
|
119
|
+
#
|
120
|
+
# However, you can still change the rows and write them back to database:
|
121
|
+
# posts.first.title = "new title"
|
122
|
+
# posts.first.write
|
66
123
|
def select( conditions = {}, options = {} )
|
67
124
|
initialize_table unless initialized
|
68
125
|
row_class = build_row_class( options )
|
@@ -74,6 +131,8 @@ module Momomoto
|
|
74
131
|
data
|
75
132
|
end
|
76
133
|
|
134
|
+
# experimental
|
135
|
+
#
|
77
136
|
# Searches for records and returns an array containing the records
|
78
137
|
def select_outer_join( conditions = {}, options = {} )
|
79
138
|
initialize_table unless initialized
|
@@ -131,10 +190,14 @@ module Momomoto
|
|
131
190
|
new_row
|
132
191
|
end
|
133
192
|
|
134
|
-
# Tries to
|
193
|
+
# Tries to select the specified record or creates a new one if it does not find it.
|
135
194
|
# Raises an exception if multiple records are found.
|
136
195
|
# You can pass a block which has to deliver the respective values for the
|
137
|
-
# primary key fields
|
196
|
+
# primary key fields.
|
197
|
+
#
|
198
|
+
# # selects the feed row matching the specified URL or creates a new
|
199
|
+
# # row based on the given URL.
|
200
|
+
# Feeds.select_or_new( :url => "https://www.c3d2.de/news-atom.xml" )
|
138
201
|
def select_or_new( conditions = {}, options = {} )
|
139
202
|
begin
|
140
203
|
if block_given?
|
@@ -157,19 +220,19 @@ module Momomoto
|
|
157
220
|
end
|
158
221
|
|
159
222
|
# Select a single row from the database
|
160
|
-
# raises Momomoto::Nothing_found if
|
161
|
-
# Momomoto::Too_many_records if more than one record was found.
|
223
|
+
# raises Momomoto::Nothing_found if no row matched.
|
224
|
+
# raises Momomoto::Too_many_records if more than one record was found.
|
162
225
|
def select_single( conditions = {}, options = {} )
|
163
226
|
data = select( conditions, options )
|
164
227
|
case data.length
|
165
|
-
when 0 then raise Nothing_found, "nothing found in #{
|
228
|
+
when 0 then raise Nothing_found, "nothing found in #{full_name}"
|
166
229
|
when 1 then return data[0]
|
167
|
-
else raise Too_many_records, "too many records found in #{
|
230
|
+
else raise Too_many_records, "too many records found in #{full_name}"
|
168
231
|
end
|
169
232
|
end
|
170
233
|
|
171
|
-
#
|
172
|
-
#
|
234
|
+
# Writes row back to database.
|
235
|
+
# This method is called by Momomoto::Row#write
|
173
236
|
def write( row ) # :nodoc:
|
174
237
|
if row.new_record?
|
175
238
|
insert( row )
|
@@ -181,8 +244,8 @@ module Momomoto
|
|
181
244
|
true
|
182
245
|
end
|
183
246
|
|
184
|
-
#
|
185
|
-
#
|
247
|
+
# Creates an insert statement for a row.
|
248
|
+
# Do not use it directly but use row.write or Table.write(row) instead.
|
186
249
|
def insert( row )
|
187
250
|
fields, values = [], []
|
188
251
|
columns.each do | field_name, datatype |
|
@@ -205,8 +268,21 @@ module Momomoto
|
|
205
268
|
database.execute( sql )
|
206
269
|
end
|
207
270
|
|
208
|
-
#
|
209
|
-
#
|
271
|
+
# Creates an update statement for a row.
|
272
|
+
# Do not call update directly but use row.write or Table.write(row) instead.
|
273
|
+
#
|
274
|
+
# Get the value of row.new_record? before writing to database to find out
|
275
|
+
# if you are updating a row that already exists in the database.
|
276
|
+
#
|
277
|
+
# feed = Feeds.select_single( :url => "http://www.c3d2.de/news-atom.xml" )
|
278
|
+
# feed.new_record? => false
|
279
|
+
# feed[:url] = "https://www.c3d2.de/news-atom.xml"
|
280
|
+
# feed.new_record? => false
|
281
|
+
#
|
282
|
+
# feed = Feeds.select_or_new( :url => "http://astroblog.spaceboyz.net/atom.rb" )
|
283
|
+
# feed.new_record? => true
|
284
|
+
# feed.write => true
|
285
|
+
# feed.new_record? => false
|
210
286
|
def update( row )
|
211
287
|
raise CriticalError, 'Updating is only allowed for tables with primary keys' if primary_keys.empty?
|
212
288
|
setter, conditions = [], {}
|
@@ -222,7 +298,7 @@ module Momomoto
|
|
222
298
|
database.execute( sql )
|
223
299
|
end
|
224
300
|
|
225
|
-
# delete _row_ from
|
301
|
+
# delete _row_ from table
|
226
302
|
def delete( row )
|
227
303
|
raise CriticalError, 'Deleting is only allowed for tables with primary keys' if primary_keys.empty?
|
228
304
|
raise Error, "this is a new record" if row.new_record?
|
@@ -243,7 +319,7 @@ module Momomoto
|
|
243
319
|
classname.split('::').last.downcase.gsub(/[^a-z_0-9]/, '')
|
244
320
|
end
|
245
321
|
|
246
|
-
#
|
322
|
+
# initializes a table class
|
247
323
|
def initialize_table
|
248
324
|
|
249
325
|
@table_name ||= construct_table_name( self.name )
|
@@ -266,7 +342,17 @@ module Momomoto
|
|
266
342
|
|
267
343
|
end
|
268
344
|
|
269
|
-
#
|
345
|
+
# Builds the row class for this table when executing #select.
|
346
|
+
# In the default Row class proper setter and getter are available
|
347
|
+
# for all columns. However, if you only want to select a few
|
348
|
+
# columns as in
|
349
|
+
#
|
350
|
+
# Feeds.select( {}, {:columns => [:url,:last_changed]} )
|
351
|
+
#
|
352
|
+
# #build_row_class does not return the default Row class for this table
|
353
|
+
# but invokes Base#initialize_row to create a new class without
|
354
|
+
# the setter and getter for the unused columns.
|
355
|
+
# For better performance the newly created class is cached in +@row_cache+.
|
270
356
|
def build_row_class( options )
|
271
357
|
if options[:columns]
|
272
358
|
options[:columns] += primary_keys
|
@@ -286,7 +372,7 @@ module Momomoto
|
|
286
372
|
end
|
287
373
|
end
|
288
374
|
|
289
|
-
#
|
375
|
+
# compiles the select clause
|
290
376
|
def compile_select( conditions, options )
|
291
377
|
if options[:columns]
|
292
378
|
cols = {}
|
data/lib/timeinterval.rb
CHANGED
@@ -1,31 +1,71 @@
|
|
1
1
|
|
2
2
|
require 'date'
|
3
3
|
|
4
|
-
#
|
4
|
+
# The class is used in Momomoto to represent time intervals.
|
5
5
|
class TimeInterval
|
6
6
|
|
7
7
|
include Comparable
|
8
8
|
|
9
|
+
# Is raised if a String cannot be converted to some representation of
|
10
|
+
# TimeInterval.
|
11
|
+
#
|
12
|
+
# TimeInterval.new( {:hour => '42', :min => '23'} )
|
13
|
+
# => #<TimeInterval:0x505c565c @hour="42", @sec=0, @min="23">
|
14
|
+
# TimeInterval.new('invalid')
|
15
|
+
# => TimeInterval::ParseError: Could not parse interval 'invalid'
|
9
16
|
class ParseError < StandardError; end
|
10
17
|
|
18
|
+
# Getter methods for hours, minutes and seconds
|
19
|
+
#
|
20
|
+
# interval = TimeInterval.new( {:hour => 42, :min => 23} )
|
21
|
+
# time.hour => 42
|
22
|
+
# time.min => 23
|
23
|
+
# time.sec => 0
|
11
24
|
attr_reader :hour, :min, :sec
|
12
25
|
|
13
26
|
class << self
|
14
27
|
|
28
|
+
# Creates and returns a new instance of TimeInterval from the given +interval+
|
29
|
+
# interval = TimeInterval.new( "00:23" )
|
30
|
+
# => #<TimeInterval:0x5235d458 @hour=0, @sec=0, @min=23>
|
15
31
|
def parse( interval )
|
16
32
|
TimeInterval.new( interval )
|
17
33
|
end
|
18
34
|
|
19
35
|
end
|
20
36
|
|
21
|
-
#
|
22
|
-
#
|
37
|
+
# Compares two TimeInterval instances by converting into seconds and
|
38
|
+
# applying #<=> to them.
|
39
|
+
# Returns 1 (first > last), 0 (first == last) or -1 (first < last).
|
40
|
+
#
|
41
|
+
# i1 = TimeInterval.new( {:hour => 42, :min => 23, :sec => 2} )
|
42
|
+
# i1.to_i => 152582
|
43
|
+
# i2 = TimeInterval.new( {:hour => 5, :min => 23, :sec => 2} )
|
44
|
+
# i2.to_i => 19382
|
45
|
+
#
|
46
|
+
# i1 <=> i2 #=> 1
|
47
|
+
#
|
23
48
|
def <=>( other )
|
24
49
|
self.to_i <=> other.to_i
|
25
50
|
end
|
26
51
|
|
27
|
-
#
|
28
|
-
|
52
|
+
# add to another TimeInterval instance
|
53
|
+
def +( other )
|
54
|
+
self.class.new( self.to_i + other.to_i )
|
55
|
+
end
|
56
|
+
|
57
|
+
# subtract something to a TimeInterval instance
|
58
|
+
def -( other )
|
59
|
+
self.class.new( self.to_i - other.to_i )
|
60
|
+
end
|
61
|
+
|
62
|
+
# Formats time interval according to the directives in the given format
|
63
|
+
# string.
|
64
|
+
#
|
65
|
+
# i = TimeInterval.new(2342) #2342 seconds
|
66
|
+
# i.strftime( "%H" ) => "00"
|
67
|
+
# i.strftime( "%M" ) => "39"
|
68
|
+
# i.strftime( "%S" ) => "02"
|
29
69
|
def strftime( fmt = "%H:%M:%S" )
|
30
70
|
fmt.gsub( /%(.)/ ) do | match |
|
31
71
|
case match[1,1]
|
@@ -38,38 +78,72 @@ class TimeInterval
|
|
38
78
|
end
|
39
79
|
end
|
40
80
|
|
41
|
-
#
|
81
|
+
# Returns the value of time interval as number of seconds
|
42
82
|
def to_i
|
43
83
|
@hour * 3600 + @min * 60 + @sec
|
44
84
|
end
|
45
85
|
|
46
86
|
alias_method :to_int, :to_i
|
47
87
|
|
48
|
-
# Returns
|
88
|
+
# Returns the value of time interval as number of seconds
|
89
|
+
# Time.now + TimeInterval.new(2342)
|
90
|
+
# => Tue Dec 11 21:16:06 +0100 2007
|
91
|
+
def to_f
|
92
|
+
self.to_i.to_f
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a string representing time interval. Equivalent to calling
|
49
96
|
# Time#strftime with a format string of '%H:%M:%S'.
|
97
|
+
#
|
98
|
+
# i.inspect => "#<TimeInterval:0x517e36b8 @hour=0, @sec=2, @min=39>"
|
99
|
+
# i.to_s => "00:39:02"
|
100
|
+
# i.strftime => "00:39:02"
|
50
101
|
def to_s
|
51
102
|
strftime( '%H:%M:%S' )
|
52
103
|
end
|
53
104
|
|
105
|
+
# Creates a new instance of TimeInterval with +d+ representing either
|
106
|
+
# a value of type #Hash
|
107
|
+
#
|
108
|
+
# TimeInterval.new( {:hour => 5}),
|
109
|
+
#
|
110
|
+
# a value of type #Integer in seconds
|
111
|
+
#
|
112
|
+
# TimeInterval.new( 23 ),
|
113
|
+
#
|
114
|
+
# or of type #String
|
115
|
+
#
|
116
|
+
# TimeInterval.new( "00:23" ).
|
117
|
+
#
|
118
|
+
# Use getter methods #hour, #min and #sec in your code.
|
54
119
|
def initialize( d = {} )
|
55
120
|
case d
|
56
121
|
when Hash then
|
57
|
-
|
58
|
-
@min = d[:min] || 0
|
59
|
-
@sec = d[:sec] || 0
|
122
|
+
init_from_hash( d )
|
60
123
|
when Integer then
|
61
|
-
|
62
|
-
@min = (d/60)%60
|
63
|
-
@sec = d%60
|
124
|
+
init_from_int( d )
|
64
125
|
when String then
|
65
126
|
parsed = Date._parse( d, false)
|
66
127
|
if ( parsed.empty? && d.length > 0 ) || !(parsed.keys - [:hour,:min,:sec,:sec_fraction]).empty?
|
67
128
|
raise ParseError, "Could not parse interval `#{d}`"
|
68
129
|
end
|
69
|
-
|
70
|
-
@min = parsed[:min] || 0
|
71
|
-
@sec = parsed[:sec] || 0
|
130
|
+
init_from_hash( parsed )
|
72
131
|
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
protected
|
136
|
+
|
137
|
+
def init_from_hash( d ) #:nodoc:
|
138
|
+
@hour = Integer( d[:hour] || 0 )
|
139
|
+
@min = Integer( d[:min] || 0 )
|
140
|
+
@sec = Integer( d[:sec] || 0 )
|
141
|
+
end
|
142
|
+
|
143
|
+
def init_from_int( d ) #:nodoc:
|
144
|
+
@hour = d/3600
|
145
|
+
@min = (d/60)%60
|
146
|
+
@sec = d%60
|
73
147
|
end
|
74
148
|
|
75
149
|
end
|