Ruby — как генерировать случайные временные интервалы, соответствующие общему количеству часов?

#ruby

#ruby

Вопрос:

Я пытаюсь написать простой скрипт, в котором входными данными будут дата начала, дата окончания и общее количество часов ( 150 ), и скрипт сгенерирует простой отчет, содержащий случайные интервалы времени (в идеале — дни недели), которые суммируют введенное количество часов.

Это то, чего я пытаюсь достичь:

 Start: 2020-01-01
End: 2020-01-31
Total hours: 150

Report:
Jan 1, 2019, 08:02:20 – Jan 1, 2019, 08:55:00: sub time -> 52:40 (52 minutes 40 seconds)
Jan 1, 2019, 09:00:00 – Jan 1, 2019, 09:38:13: sub time -> 38:13 (38 minutes 13 seconds)
...
Jan 3, 2019, 13:15:00 – Jan 3, 2019, 14:45:13: sub time -> 01:30:13 (1 hour 30 minutes 13 seconds)
...

TOTAL TIME: 150 hours (or in minutes)
  

Как мне генерировать временные интервалы, в которых общее количество минут / часов будет равно заданному количеству часов?

Комментарии:

1. Это нетривиально, потому что это, по сути, проблема с рюкзаком. Вы можете генерировать даты и время в диапазоне с помощью Faker gem, но вам придется приложить некоторые усилия, чтобы определить, как вы планируете рандомизировать свои результаты, при этом получая ровно 150 часов. ПРИМЕЧАНИЕ: Если это для целей тестирования, это, вероятно, проблема X / Y.

Ответ №1:

Я предполагаю, что вопрос сформулирован свободно в том смысле, что «случайный» не подразумевается в смысле вероятности; то есть цель состоит не в том, чтобы выбрать набор интервалов (которые составляют заданное количество часов в длину) с механизмом, который гарантирует, что все возможные наборы таких интервалов имеют одинаковую длину.равная вероятность выбора. Скорее, я понимаю, что набор интервалов должен быть выбран (например, для целей тестирования) таким образом, чтобы включать элементы случайности.

Я предположил, что интервалы не должны перекрываться, и количество интервалов должно быть указано. Я не понимаю, что означает «с идеальными будними днями», поэтому я проигнорировал это.


Суть подхода, который я предложу, заключается в следующем методе.

 def rnd_lengths(tot_secs, target_nbr)      
  max_secs = 2 * tot_secs/target_nbr - 1
  arr = []
  loop do
    break(arr) if tot_secs.zero?
    l = [(0.5   max_secs * rand).round, tot_secs].min
    arr << l
    tot_secs -= l
  end
end
  

Метод генерирует массив целых чисел (длин интервалов), измеряемых в секундах, в идеале имеющих target_nbr элементы. tot_secs это требуемая общая длина «случайных» интервалов (например, 150 * 3600).

Каждый элемент массива извлекается случайным образом из равномерного распределения, которое варьируется от нуля до max_secs (подлежит вычислению). Это делается последовательно до tot_secs тех пор, пока не будет достигнуто. Если последнее случайное значение приведет к превышению общего tot_secs значения, оно уменьшается, чтобы сделать общее значение равным tot_secs . `

Предположим tot_secs , что равно 100 и мы хотим генерировать 4 случайные интервалы ( target_nbr = 4 ). Это означает, что средняя длина интервалов будет 25 . Поскольку мы используем равномерное распределение, имеющее среднее значение (1 max_secs)/2 , мы можем получить значение max_secs из выражения

 target_nbr * (1   max_secs)/2 = tot_secs
  

что такое

 max_secs = 2 * tot_secs/target_nbr - 1
  

первая строка метода. Для примера, который я упомянул, это будет

 max_secs = 2 * 100/4 - 1
  #=> 49
  

Давайте попробуем.

 rnd_lengths(100, 4)
  #=> [49, 36, 15]
  

Как вы видите, возвращаемый массив суммируется по 100 мере необходимости, но он содержит только 3 элементы. Вот почему я назвал аргумент target_nbr , поскольку нет гарантии, что возвращаемый массив будет содержать такое количество элементов. Что делать? Попробуйте еще раз!

 rnd_lengths(100, 4)
  #=> [14, 17, 26, 37, 6] 
  

Все еще не 4 элементы, так что продолжайте пытаться:

 rnd_lengths(100, 4)
  #=> [11, 37, 39, 13] 
  

Успех! Может потребоваться несколько попыток, чтобы получить правильное количество элементов, но для параметров, которые могут быть использованы, и характера используемого распределения вероятностей я бы не ожидал, что это будет проблемой.

Давайте поместим это в метод.

 def rdm_intervals(tot_secs, nbr_intervals)
  loop do
    arr = rnd_lengths(tot_secs, nbr_intervals) 
    break(arr) if arr.size == nbr_intervals
  end
end

intervals = rdm_intervals(100, 4)
  #=> [29, 26, 7, 38]
  

Мы можем вычислять случайные промежутки между интервалами таким же образом. Предположим, что интервалы попадают в диапазон 175 секунд (количество секунд между временем начала и временем окончания). Затем:

 gaps = rdm_intervals(175-100, 5)
  #=> [26, 5, 19, 4, 21]
  

Как видно, пробелы суммируются по 75 мере необходимости. Мы можем пренебречь последним элементом.


Теперь мы можем формировать интервалы. Первый интервал начинается в 26 секундах и заканчивается в 26 29 #=> 55 секундах. Второй интервал начинается в 55 5 #=> 60 секундах и заканчивается в 60 26 #=> 86 секундах, и так далее. Поэтому мы находим интервалы (каждый в интервалах секунд от нуля) равными:

 [26..55, 60..86, 105..112, 116..154]
  

Обратите внимание, что 175 - 154 = 21 последний элемент gaps .


Если кого-то смущает тот факт, что последние элементы intervals и gaps , которые обычно ограничены по размеру, можно, конечно, случайным образом переместить эти элементы в их соответствующие массивы.

Может быть, кому-то все равно, точно ли указано количество интервалов target_nbr . Было бы проще и быстрее просто использовать первый созданный массив длин интервалов. Это нормально, но нам все еще нужны вышеуказанные методы для вычисления случайных промежутков, поскольку их количество должно равняться количеству интервалов плюс один:

 gaps = rdm_intervals(175-100, intervals.size   1)
  

Теперь мы можем использовать эти два метода для построения метода, который вернет желаемый результат. Аргумент tot_secs этого метода равен общему количеству секунд, охватываемых возвращаемыми интервалами массива (например, 3600 * 150 ). Метод возвращает массив, содержащий nbr_intervals неперекрывающиеся диапазоны Time объектов, которые находятся между заданными начальными и конечными датами.

 require 'date'
  
 def construct_intervals(start_date_str, end_date_str, tot_secs, nbr_intervals)
  start_time = Date.strptime(start_date_str, '%Y-%m-%d').to_time
  secs_in_period = Date.strptime(end_date_str, '%Y-%m-%d').to_time - start_time
  intervals = rdm_intervals(tot_secs, nbr_intervals)
  gaps = rdm_intervals(secs_in_period - tot_secs, nbr_intervals 1)
  nbr_intervals.times.with_object([]) do |_,arr|
    start_time  = gaps.shift
    end_time = start_time   intervals.shift
    arr << (start_time..end_time)
    start_time = end_time
  end
end
  

Смотрите Date::strptime .


Давайте попробуем пример.

 start_date_str = '2020-01-01'
end_date_str   = '2020-01-31' 
tot_secs       = 3600*150
  #=> 540000
  
 construct_intervals(start_date_str, end_date_str, tot_secs, 4)
  #=> [2020-01-06 18:05:04 -0800..2020-01-09 03:48:00 -0800,
  #    2020-01-09 06:44:16 -0800..2020-01-11 23:33:44 -0800,
  #    2020-01-20 20:30:21 -0800..2020-01-21 17:27:44 -0800,
  #    2020-01-27 19:08:38 -0800..2020-01-28 01:38:51 -0800] 
  
 construct_intervals(start_date_str, end_date_str, tot_secs, 8)
  #=> [2020-01-03 18:43:36 -0800..2020-01-04 10:49:14 -0800,
  #    2020-01-08 07:55:44 -0800..2020-01-08 08:17:18 -0800,
  #    2020-01-11 00:54:36 -0800..2020-01-11 23:00:53 -0800,
  #    2020-01-14 05:20:14 -0800..2020-01-14 22:48:45 -0800,
  #    2020-01-16 18:28:28 -0800..2020-01-17 22:50:24 -0800,
  #    2020-01-22 02:59:31 -0800..2020-01-22 22:33:08 -0800,
  #    2020-01-23 00:36:59 -0800..2020-01-24 12:15:37 -0800,
  #    2020-01-29 11:22:21 -0800..2020-01-29 21:46:10 -0800] 
  

Смотрите Date::strptime

Ответ №2:

 START -xxx----xxx--x----xxxxx---xx--xx---xx-xx-x-xxx-- END
  

Нам нужно заполнить временной интервал чередующимися периодами включения и выключения. Это может быть
обозначено списком временных меток. Для простоты предположим, что период всегда начинается с
периода отключения.

Начиная с начала / конца временного интервала и общего количества секунд в состоянии ON, мы собираем полезные факты:

  • общий размер временного интервала в секундах total_seconds
  • вторые итоги периодов включения ( on_total_seconds ) и выключения ( off_total_seconds )

Как только мы их узнаем, работоспособный алгоритм выглядит более или менее так — извините за функции без реализации:

 # this can be a parameter as well
MIN_PERIODS = 10
MAX_PERIODS = 100

def fill_periods(start_date, end_date, on_total_seconds = 150*60*60)
  total_seconds = get_total_seconds(start_date, end_date)
  off_total_seconds = total_seconds - on_total_seconds

  # establish two buckets to pull from alternately in populating our array of durations
  on_bucket = on_total_seconds
  off_bucket = off_total_seconds
  result = []

  # populate `result` with durations in seconds. `result` will sum to `total_seconds`
  while on_bucket > 0 || off_bucket > 0 do
    off_slice = rand(off_total_seconds / MAX_PERIODS / 2, off_total_seconds / MIN_PERIODS / 2).to_i
    off_bucket -= [off_slice, off_bucket].min

    on_slice = rand(on_total_seconds / MAX_PERIODS / 2, on_total_seconds / MIN_PERIODS / 2).to_i
    on_bucket -= [on_slice, on_bucket].min

    # randomness being random, we're going to hit 0 in one bucket before the
    # other. when this happens, just add this (off, on) pair to the last one.
    if off_slice == 0 || on_slice == 0
      last_off, last_on = result.pop(2)
      result << last_off   off_slice << last_on   on_slice
    else
      result << off_slice << on_slice
    end
  end

  # build up an array of datetimes by progressively adding seconds to the last timestamp.
  datetimes = result.each_with_object([start_date]) do |period, memo|
    memo << add_seconds(memo.last, period)
  end

  # we want a list of datetime pairs denoting ON periods. since we know our
  # timespan starts with OFF, we start our list of pairs with the second element.
  datetimes.slice(1..-1).each_slice(2).to_a
end