对一段时间段进行分类的最佳方法是什么?

时间:2015-02-20 19:47:05

标签: ruby-on-rails ruby

我正在开发一个员工rota系统。对于工资单,我需要根据轮班所涵盖的日期/时间段计算正确的工资率。

如何在不使用冗长条件语句的情况下检查各种日期(周末,假日,工作日)。

给定任何时间范围(班次):

例如。 2015-01-20 15:00 - > 2015-01-21 17:00

对这一时期的细分市场进行分类的最佳(也是最有效的方式)是什么?

我想知道:

  • 任何工作日(星期一)22:00至07:00之间(如有) 星期五)晚上。

  • 期间(如有的话)在星期六的08:00到星期日的22:00之间。

  • 公众假期(如果有的话)的期间(使用 holidays gem

那么我的两个问题是:

1)知道一个时间段(班次)可以跨越一个周末(虽然我更喜欢一个可以支持多天的解决方案),我该如何计算要比较的日期/时间范围?

2)一旦我确定要比较的时间段(周末,假期等),我如何最好地确定这些时期的交集并确定它们的持续时间?

3 个答案:

答案 0 :(得分:0)

我不完全理解您的问题,所以我已经汇总了一些基于对您所面临问题的许多假设的代码。我希望我所解决的一些问题以及我处理这些问题的方式可能对您有所帮助。例如,如果工人在轮班结束时仍在工作,则有必要确定下一轮班次,这可能是第二天。

您会发现我的代码非常粗糙且结构不合理。我有很多临时变量,只是为了帮助你了解正在发生的事情。在真实的应用程序中,您可能希望添加一些类,更多方法等。另外,假设数据将存储在数据库中,您可能希望使用SQL进行某些计算。

首先,我假设是数据。

数据

假期列表:

holidays = ["2015:04:05", "2015:04:06"]

每位员工的信息,包括员工的职位分类,关键是员工ID:

employees = {
  123 => { name: 'Bob', job: :worker_bee1 },
  221 => { name: 'Sue', job: :worker_bee2 }
}

每天有相同日期的团体,每个工作日和当天的工作日期间的工资率相同,除非当天是假日:

day_groups = { weekdays: [1,2,3,4,5], weekends: [6,0] }

每个工作期的信息:

work_periods = {  
  weekday_graveyard: {
    day_group: :weekdays,
    start_time: "00:00", 
    end_time:   "08:00" },
  weekday_day: {
    day_group: :weekdays,
    start_time: "08:00", 
    end_time:   "16:00" },
  weekday_swing: {
    day_group: :weekdays,
    start_time: "16:00", 
    end_time:   "24:00" },
  weekend: {
    day_group: :weekends,
    start_time: "00:00", 
    end_time:   "24:00" } }

按工作,透析期,非假期和假期的工资表:

wage_by_job = {
  worker_bee1: {
    weekday_graveyard: { standard: 30.00, holiday: 60.00 },
    weekday_day:       { standard: 20.00, holiday: 40.00 },
    weekday_swing:     { standard: 25.00, holiday: 50.00 },
    weekend:           { standard: 22.00, holiday: 44.00 }
  },
  worker_bee2: {
    weekday_graveyard: { standard: 32.00, holiday: 64.00 },
    weekday_day:       { standard: 22.00, holiday: 44.00 },
    weekday_swing:     { standard: 27.00, holiday: 54.00 },
    weekend:           { standard: 24.00, holiday: 48.00 }
  }
}

所有员工在薪资期间工作的小时数:

shifts_worked = [
  { id: 123, date: "2015:04:03", start_time: "15:30", end_time: "00:15" },
  { id: 221, date: "2015:04:04", start_time: "23:30", end_time: "08:30" },
  { id: 123, date: "2015:04:06", start_time: "08:00", end_time: "16:00" },
  { id: 221, date: "2015:04:06", start_time: "23:00", end_time: "07:00" },
  { id: 123, date: "2015:04:07", start_time: "00:00", end_time: "09:00" }
]

<强>助手

require 'set'
require 'date'

def date_str_to_obj(date_str)
  Date.strptime(date_str, '%Y:%m:%d')
end

  date_str_to_obj("2015:04:04")
    #=> #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>

def next_day(day)
  (day==6) ? 0 : day+1
end

  next_day(6)
    #=> 0

将假日转换为日期对象并存储在一组中以便快速查找:

@holiday_set = Set.new(holidays.map { |date_str| 
  date_str_to_obj(date_str) }.to_set)
  #=> #<Set: {#<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>,
  #           #<Date: 2015-04-06 ((2457119j,0s,0n),+0s,2299161j)>}> 

def is_holiday?(date)
  @holiday_set.include?(date)
end 

  is_holiday?(date_str_to_obj("2015:04:04"))
    #=> false
  is_holiday?(date_str_to_obj("2015:04:05"))
    #=> true

将每周的每一天映射到day_groups

元素
@day_group_by_dow = day_groups.each_with_object({}) { |(period,days),h|
  days.each { |d| h[d] = period } }
  #=> {1=>:weekdays, 2=>:weekdays, 3=>:weekdays, 4=>:weekdays,
  #    5=>:weekdays, 6=>:weekend,  0=>:weekend} 

day_groups的每个元素映射到一系列工作时段:

@work_periods_by_day_group = work_periods.each_with_object({}) { |(k,g),h|
  h.update(g[:day_group]=>[k]) { |_,nwp,owp| nwp+owp } }
  #=> {:weekdays=>[:weekday_graveyard, :weekday_day, :weekday_swing],
  #    :weekend=> [:weekends]} 

计算工作时间内的工作时间:

def start_and_end_times_to_hours(start_time, end_time)
  (time_to_minutes_after_midnight(end_time) - 
    time_to_minutes_after_midnight(start_time))/60.0
end

  start_and_end_times_to_hours("10:00", "14:30")
    #=> 4.5

上一种方法的帮助:

private    
def time_to_minutes_after_midnight(time_str)
  hrs, mins = time_str.scan(/(\d\d):(\d\d)/).first.map(&:to_i)      
  60 * hrs + mins
end
public

  time_to_minutes_after_midnight("10:00")
    #=> 600
  time_to_minutes_after_midnight("14:30")
    #=> 870

如方法名称所示:

def date_and_time_to_current_period(date, time, work_periods)
  day_grp = @day_group_by_dow[date.wday]
  periods = @work_periods_by_day_group[day_grp]
  periods.find do |per|
    p = work_periods[per]
    p[:start_time] <= time && time < p[:end_time]
  end
end

  date_and_time_to_current_period(date_str_to_obj("2015:04:03"),
    #=> :weekday_swing 
  date_and_time_to_current_period(date_str_to_obj("2015:04:04"),
    #=> :weekend 

最后,另一个不言自明的方法:

def next_period_and_date_by_period_and_date(work_periods, period, date)
  end_time = work_periods[period][:end_time]
  if end_time == "24:00" # next_day
    day_grp = @day_group_by_dow[next_day(date.wday)]
    wp = @work_periods_by_day_group[day_grp]
    [wp.find { |period| work_periods[period][:start_time]=="00:00" }, date+1]
  else # same day
    day_grp = work_periods[period][:day_group]
    wp = @work_periods_by_day_group[day_grp]
    [wp.find { |period| work_periods[period][:start_time]==end_time }, date] 
  end
end

  next_period_and_date_by_period_and_date(work_periods,
    :weekday_day, date_str_to_obj("2015:04:03"))
    #=> [:weekday_swing, #<Date: 2015-04-03 ((2457116j,0s,0n),+0s,2299161j)>] 
  next_period_and_date_by_period_and_date(work_periods,
    :weekday_swing, date_str_to_obj("2015:04:02"))
    #=> [:weekday_graveyard, #<Date: 2015-04-03...+0s,2299161j)>] 
  next_period_and_date_by_period_and_date(work_periods,
    :weekday_swing, date_str_to_obj("2015:04:03"))
    #=> [:weekend, #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>] 
  next_period_and_date_by_period_and_date(work_periods,
    :weekday_swing, date_str_to_obj("2015:04:04"))
    #=> [:weekend, #<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>] 

计算工资单

shifts_worked.each_with_object(Hash.new(0.0)) do |shift, payroll|
  id = shift[:id]
  date = date_str_to_obj(shift[:date])
  start_time = shift[:start_time]
  end_time = shift[:end_time]
  wage_schedule = wage_by_job[employees[id][:job]]
  curr_period = date_and_time_to_current_period(date, start_time, work_periods)

  while true
    end_time_in_period = work_periods[curr_period][:end_time]
    end_time_in_period = end_time if
      (end_time > start_time && end_time < end_time_in_period)
    hours_in_period =
      start_and_end_times_to_hours(start_time, end_time_in_period)
    wage = wage_schedule[curr_period][is_holiday?(date) ? :holiday : :standard]
    payroll[id] += (wage * hours_in_period).round(2)
    break if end_time == end_time_in_period
    curr_period, date =
      next_period_and_date_by_period_and_date(work_periods,
        curr_period, date)
    start_time = work_periods[curr_period][:start_time]
  end
end
  #=> {123=>795.5, 221=>698.0}

答案 1 :(得分:0)

我使用了以下gem: https://github.com/fnando/recurrence

我还没有完成假期。

要求

class Requirement < ActiveRecord::Base
    # Model with attributes:
    # start - datetime
    # end - datetime
    # is_sleepin - boolean

    def duration
        self.end - self.start
    end

    def to_range
        self.start..self.end
    end

   def spans_multiple_days?
      self.end.to_date != self.start.to_date
   end

end

要求持续时间的细分(转移)

class Breakdown

  attr_reader :requirement,
              :standard_total_duration,
              :weekend_total_duration,
              :wakein_total_duration

  def initialize(requirement)
    @requirement = requirement

    @rules = Array.new
    @rules << Breakdown::StandardRule.new(self)
    @rules << Breakdown::WeekendRule.new(self)

    @standard_total_duration = components[:standard].total_duration
    @weekend_total_duration = components[:weekend].total_duration

    if @requirement.is_sleepin?
      @standard_total_duration = 0
      @weekend_total_duration = 0
    end

    # Following is a special set of rules for certain Locations where staff work
    # If a requirement is_sleepin? that means duration is not counted so set to 0

    if ['Home 1', 'Home 2'].include?(@requirement.home.name.strip) and
      @requirement.spans_multiple_days? and not @requirement.is_sleepin?

        @rules << Aspirations::Breakdown::WakeinRule.new(self)
        @wakein_total_duration = components[:wakein].total_duration
        @standard_total_duration = 0
        @weekend_total_duration = 0
    end

  end

  def components
    @rules.hmap{ |k,v| [ k.to_sym, k ] }
  end

end

使用规则来指定轮班持续时间的哪些部分应该分类:

module Breakdown
class Rule

  def initialize(breakdown)
    @requirement = breakdown.requirement
  end

  def to_sym
    # eg 'Breakdown::StandardRule' becomes :standard
    self.class.name.split('::').last.gsub("Rule", "").downcase.to_sym
  end

  def periods
    output = []
    occurrences = rule.events.map{ |e| rule_time_range(e) }
    occurrences.each do |o|
      if (o.max > @requirement.start and @requirement.end > o.min)
        output << (o & @requirement.to_range)
      end
    end
    return output
  end

  def total_duration
    periods.reduce(0) { |sum, p| sum + (p.max - p.min).to_i }
  end

end
end

规则示例(在本例中为周末规则)

module Breakdown
class WeekendRule < Breakdown::Rule

  def period_expansion
    # This is an attempt to safely ensure that a weekend period
    # is detected even though the start date of the requirement
    # may be on Sunday evening, maybe could be just 2 days
    4.days
  end

  def period_range
    (@requirement.start.to_date - period_expansion)..(@requirement.end.to_date + period_expansion)
  end

  def rule
    Recurrence.new(:every => :week, :on => :saturday, :starts => period_range.min, :until => period_range.max)
  end

  def rule_time_range(date)
    # Saturday 8am
    start = date + Time.parse("08:00").seconds_since_midnight.seconds
    _end = (date + 1.day) + Time.parse("22:00").seconds_since_midnight.seconds
    start.._end
  end

end
end

答案 2 :(得分:0)

一种可能的方法可能是将一周打破到15分钟(取决于您需要多少分辨率)的单个块。而不是有些难以处理的时间范围,你可以使用这些块的集合,Ruby非常好地支持。

按时间顺序编号时间块:

星期一12:00 AM-12:15 AM = 0
星期一12:15 AM-12:30 AM = 1
...
周日11:45 PM-12:00 AM = 671

然后为假期,周末,每个班次等准备一组整数,无论​​你需要什么。除了假期,这些可能都是常数。

例如,从周六上午8点到周日晚上10点的周末:

周末= [512..663]

同样,将每位员工的出勤率表示为一组。例如,周一上午8点到中午以及周六上午10点到11点工作的人将是:

出勤率= [32..47,520..523]

通过这种方法,您可以使用设置交叉点来计算周末的小时数:

周末出席=周末&amp;出勤