#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