time_calculation.rb 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. module TimeCalculation
  2. =begin
  3. put working hours matrix and timezone in function, returns UTC working hours matrix
  4. working_hours_martix = TimeCalculation.working_hours('2013-10-27 20:00:15', working_hours_matrix, 'Europe/Berlin')
  5. working_hours_martix = {
  6. :Mon => [nil,nil,nil,nil,nil,nil,nil,nil,true,true,true,true,true,true,true,true,true,true,true,nil,nil,nil,nil,nil],
  7. :Tue => [nil,nil,nil,nil,nil,nil,nil,nil,true,true,true,true,true,true,true,true,true,true,true,nil,nil,nil,nil,nil],
  8. :Wed => [nil,nil,nil,nil,nil,nil,nil,nil,true,true,true,true,true,true,true,true,true,true,true,nil,nil,nil,nil,nil],
  9. :Thu => [nil,nil,nil,nil,nil,nil,nil,nil,true,true,true,true,true,true,true,true,true,true,true,nil,nil,nil,nil,nil],
  10. :Fri => [nil,nil,nil,nil,nil,nil,nil,nil,true,true,true,true,true,true,true,true,true,true,true,nil,nil,nil,nil,nil],
  11. :Sat => [],
  12. :Sun => [],
  13. }
  14. =end
  15. def self.working_hours(start_time, config, timezone)
  16. time_diff = 0
  17. if timezone
  18. begin
  19. time_diff = Time.zone.parse(start_time.to_s).in_time_zone(timezone).utc_offset
  20. rescue => e
  21. Rails.logger.error "Can't fine tomezone #{timezone}"
  22. Rails.logger.error e.inspect
  23. Rails.logger.error e.backtrace
  24. end
  25. end
  26. beginning_of_workday = Time.zone.parse("1977-10-27 #{config['beginning_of_workday']}")
  27. end_of_workday = Time.zone.parse("1977-10-27 #{config['end_of_workday']}") - 3600
  28. config_ok = false
  29. working_hours = {}
  30. [:Mon, :Tue, :Wed, :Thu, :Fri, :Sat, :Sun].each {|day|
  31. working_hours[day] = []
  32. next if !config[day.to_s]
  33. if config[day.to_s] != true && config[day.to_s] != day.to_s
  34. next
  35. end
  36. config_ok = true
  37. (0..23).each {|hour|
  38. time = Time.zone.parse("1977-10-27 #{hour}:00:00")
  39. if time >= beginning_of_workday && time <= end_of_workday
  40. working_hours[day].push true
  41. else
  42. working_hours[day].push nil
  43. end
  44. }
  45. }
  46. if !config_ok
  47. fail 'sla config is invalid! ' + config.inspect
  48. end
  49. # shift working hours / if needed
  50. if time_diff && time_diff != 0
  51. hours_to_shift = (time_diff / 3600 ).round
  52. move_items = {
  53. Mon: [],
  54. Tue: [],
  55. Wed: [],
  56. Thu: [],
  57. Fri: [],
  58. Sat: [],
  59. Sun: [],
  60. }
  61. (1..hours_to_shift).each {
  62. working_hours.each {|day, value|
  63. next if !value
  64. to_move = working_hours[day].shift
  65. if day == :Mon
  66. move_items[:Tue].push to_move
  67. elsif day == :Tue
  68. move_items[:Wed].push to_move
  69. elsif day == :Wed
  70. move_items[:Thu].push to_move
  71. elsif day == :Thu
  72. move_items[:Fri].push to_move
  73. elsif day == :Fri
  74. move_items[:Sat].push to_move
  75. elsif day == :Sat
  76. move_items[:Sun].push to_move
  77. elsif day == :Sun
  78. move_items[:Mon].push to_move
  79. end
  80. }
  81. }
  82. move_items.each {|day, value|
  83. value.each {|item|
  84. working_hours[day].push item
  85. }
  86. }
  87. end
  88. working_hours
  89. end
  90. =begin
  91. returns business hours in minutes between to dates
  92. business_hours_in_min = Time.Calculation.business_time_diff(
  93. '2013-10-27 14:00:15',
  94. '2013-10-27 18:10:15',
  95. working_hours_martix,
  96. 'Europe/Berlin',
  97. )
  98. =end
  99. def self.business_time_diff(start_time, end_time, config = nil, timezone = '')
  100. if start_time.class == String
  101. start_time = Time.zone.parse( start_time.to_s + 'UTC' )
  102. end
  103. if end_time.class == String
  104. end_time = Time.zone.parse( end_time.to_s + 'UTC' )
  105. end
  106. # if no config is given, just return calculation directly
  107. if !config
  108. return ((end_time - start_time) / 60 ).round
  109. end
  110. working_hours = self.working_hours(start_time, config, timezone)
  111. week_day_map = {
  112. 1 => :Mon,
  113. 2 => :Tue,
  114. 3 => :Wed,
  115. 4 => :Thu,
  116. 5 => :Fri,
  117. 6 => :Sat,
  118. 0 => :Sun,
  119. }
  120. count = 0
  121. calculation = true
  122. first_loop = true
  123. while calculation
  124. week_day = start_time.wday
  125. day = start_time.day
  126. month = start_time.month
  127. year = start_time.year
  128. hour = start_time.hour
  129. # check if it's vacation day
  130. if config
  131. if config['holidays']
  132. if config['holidays'].include?("#{year}-#{month}-#{day}")
  133. # jump to next day
  134. start_time = start_time.beginning_of_day + 86_400
  135. next
  136. end
  137. end
  138. end
  139. # check if it's countable day
  140. if working_hours[ week_day_map[week_day] ].empty?
  141. # jump to next day
  142. start_time = start_time.beginning_of_day + 86_400
  143. next
  144. end
  145. # fillup to first full hour
  146. if first_loop
  147. diff = end_time - start_time
  148. if diff > 59 * 60
  149. diff = start_time - start_time.beginning_of_hour
  150. end
  151. start_time += diff
  152. # check if it's countable hour
  153. if working_hours[ week_day_map[week_day] ][ hour ]
  154. count += diff
  155. end
  156. end
  157. first_loop = false
  158. # loop to next hour
  159. (hour..23).each { |next_hour|
  160. # check if end time is lower
  161. if start_time >= end_time
  162. calculation = false
  163. break
  164. end
  165. # check if end_time is within this hour
  166. diff = end_time - start_time
  167. if diff > 59 * 60
  168. diff = 3600
  169. end
  170. # keep it in current day
  171. if next_hour == 23
  172. start_time += diff - 1
  173. else
  174. start_time += diff
  175. end
  176. # check if it's business hour and count
  177. if working_hours[ week_day_map[week_day] ][ next_hour ]
  178. count += diff
  179. end
  180. }
  181. # loop to next day
  182. start_time = start_time.beginning_of_day + 86_400
  183. end
  184. diff = count / 60
  185. diff.round
  186. end
  187. =begin
  188. returns destination date of start time plus X minutes
  189. dest_time = Time.Calculation.dest_time(
  190. '2013-10-27 14:00:15',
  191. 120,
  192. working_hours_martix,
  193. 'Europe/Berlin',
  194. )
  195. =end
  196. def self.dest_time(start_time, diff_in_min, config = nil, timezone = '')
  197. if start_time.class == String
  198. start_time = Time.zone.parse( start_time.to_s + ' UTC' )
  199. end
  200. return start_time if diff_in_min == 0
  201. # if no config is given, just return calculation directly
  202. if !config
  203. return start_time + (diff_in_min * 60)
  204. end
  205. # loop
  206. working_hours = self.working_hours(start_time, config, timezone)
  207. week_day_map = {
  208. 1 => :Mon,
  209. 2 => :Tue,
  210. 3 => :Wed,
  211. 4 => :Thu,
  212. 5 => :Fri,
  213. 6 => :Sat,
  214. 0 => :Sun,
  215. }
  216. count = diff_in_min * 60
  217. calculation = true
  218. first_loop = true
  219. while calculation
  220. week_day = start_time.wday
  221. day = start_time.day
  222. month = start_time.month
  223. year = start_time.year
  224. hour = start_time.hour
  225. #puts "start outer loop #{start_time}-#{week_day}-#{year}-#{month}-#{day}-#{hour}|c#{count}"
  226. # check if it's vacation day
  227. if config
  228. if config['holidays']
  229. if config['holidays'].include?("#{year}-#{month}-#{day}")
  230. # jump to next day
  231. start_time = start_time.beginning_of_day + 86_400
  232. next
  233. end
  234. end
  235. end
  236. # check if it's countable day
  237. if working_hours[ week_day_map[week_day] ].empty?
  238. # jump to next day
  239. start_time = start_time.beginning_of_day + 86_400
  240. next
  241. end
  242. # fillup to first full hour
  243. if first_loop
  244. # get rest of this hour if diff_in_min in lower the one hour
  245. diff_to_count = 3600
  246. if diff_to_count > (diff_in_min * 60)
  247. diff_to_count = diff_in_min * 60
  248. end
  249. diff = diff_to_count - (start_time - start_time.beginning_of_hour)
  250. start_time += diff
  251. # check if it's countable hour
  252. if working_hours[ week_day_map[week_day] ][ hour ]
  253. count -= diff
  254. end
  255. # start on next hour of we moved to next
  256. if diff != 0
  257. hour += 1
  258. end
  259. end
  260. first_loop = false
  261. # loop to next hour
  262. (hour..23).each { |next_hour|
  263. diff = 3600
  264. # check if count positiv
  265. if count <= 0
  266. calculation = false
  267. break
  268. end
  269. # check if it's business hour and count
  270. if working_hours[ week_day_map[week_day] ][ next_hour ]
  271. # check if count is within this hour
  272. if count > 59 * 60
  273. diff = 3600
  274. else
  275. diff = count
  276. end
  277. count -= diff
  278. end
  279. # keep it in current day
  280. if next_hour == 23
  281. start_time += diff - 1
  282. else
  283. start_time += diff
  284. end
  285. }
  286. # check if count positiv
  287. if count <= 0
  288. calculation = false
  289. break
  290. end
  291. # loop to next day
  292. start_time = start_time.beginning_of_day + 86_400
  293. end
  294. start_time
  295. end
  296. end