Как бы вы хранили и запрашивали часы работы?

Мы создаем приложение, которое хранит «часы работы» для различных предприятий. Как проще всего представить эти данные, чтобы можно было легко проверить, открыт ли элемент?

Некоторые варианты:

  • Сегментируйте блоки (каждые 15 минут), которые вы можете пометить как «открытые/закрытые». Проверка включает в себя проверку того, установлен ли бит «открыто» на желаемое время (немного похоже на расписание поездов).
  • Сохранение списка диапазонов времени (11:00–14:00, 17:00–19:00 и т. д.) и проверка того, попадает ли текущее время в какой-либо указанный диапазон (это то, что делает наш мозг при разборе строк выше).

Есть ли у кого-нибудь опыт хранения и запроса информации о расписании и какие-либо советы?

(Есть всевозможные сумасшедшие угловые случаи, такие как «закрыто в первый вторник месяца», но мы оставим это на другой день).


person Kalid    schedule 26.09.2008    source источник
comment
Может ли кто-нибудь изменить тег/переименовать? Это не специфичный для С# вопрос, он относится ко всему .NET.   -  person Alex Lyman    schedule 27.09.2008
comment
Хорошее предложение - только что пометил.   -  person Kalid    schedule 27.09.2008
comment
Я не понимаю, как это зависит от .NET? это не привязано ни к какому языку, это архитектурно.   -  person Erik van Brakel    schedule 27.09.2008
comment
Первоначально я спросил... на С#, но Эрик прав, это скорее общий вопрос об алгоритмах.   -  person Kalid    schedule 27.09.2008


Ответы (10)


сохранять каждый непрерывный блок времени как время начала и продолжительность; это упрощает проверку, когда часы пересекают границы даты

если вы уверены, что часы работы никогда не выйдут за границы даты (т. е. никогда не будет открытой ночной распродажи или 72-часового марафона и т. д.), тогда будет достаточно времени начала/окончания

person Steven A. Lowe    schedule 26.09.2008
comment
Этот подход также является более расширяемым. При таком подходе гораздо проще добавить концепцию дней или месяцев, как и добавить список исключений (например, Открыто с 9 до 5 каждый день, кроме Рождества). - person Derek Park; 27.09.2008

Наиболее гибким решением может быть использование битового подхода. В неделе 168 часов, значит, 672 периода по 15 минут. Это всего 84 байта пространства, что должно быть терпимо.

person Greg Hewgill    schedule 26.09.2008
comment
Ухудшил это, потому что это пахнет кодовым запахом, а детали реализации слишком сложны. - person Alex Lyman; 27.09.2008
comment
+1, так как он не пахнет кодовым запахом и не слишком сложен - он на самом деле довольно элегантный с точки зрения хранения. - person Rob; 27.09.2008
comment
Другие решения кажутся мне столь же вонючими, это кажется довольно разумным. Особенно если правильно абстрагироваться. - person Wedge; 27.09.2008
comment
Я сомневаюсь в этом подходе в особых случаях — что, если что-то начнется в 5:05 (например?). Мы могли бы округлить до 5:00 или 5:15, но все же... - person Kalid; 27.09.2008
comment
Это плохо ломается, когда вам нужно иметь дело с особыми случаями. Как только вам нужно сказать 9-5, кроме Рождества, ваше хранилище уменьшится с 672 до 672 * 52. Хуже, если вы разрешите планирование за пределами одного года. В нем есть определенная элегантность, но его нельзя обобщить за пределы очень конечного промежутка времени. - person Derek Park; 27.09.2008
comment
если ваши часы или работа не укладываются в 15-минутные рамки, она разваливается; учитывая, что это маловероятно. Что меня беспокоит в этом решении, так это то, как вы сформулируете запрос к нему без большого количества битов? - person Steven A. Lowe; 28.09.2008
comment
Я тоже склонялся к этому, но не могу представить разумный SQL-запрос. Точки на нем, не работающие в особых случаях, не имеют для меня смысла. Создайте таблицу переопределения, если для конкретной даты есть запись в таблице переопределения, используйте этот результат, если нет, используйте основную таблицу часов работы (psuedo). Я не гуру SQL, но я бы сказал, чтобы сделать этот ответ более законным, его нужно дополнить разумным примером подхода к SQL-запросам. Например, в моей таблице что ОТКРЫТО прямо сейчас? и т. д. - person Dave Jellison; 19.11.2013

Я бы использовал такую ​​таблицу:

BusinessID | weekDay | OpenTime | CloseTime 
---------------------------------------------
     1          1        9           13
     1          2        5           18
     1          3        5           18
     1          4        5           18
     1          5        5           18
     1          6        5           18
     1          7        5           18

Здесь у нас есть бизнес, в котором обычные часы работы с 5 до 6, но более короткие часы в воскресенье.

Запрос на открытие будет (psuedo-sql)

SELECT @isOpen = CAST
   (SELECT 1 FROM tblHours 
       WHERE BusinessId = @id AND weekDay = @Day 
       AND CONVERT(Currentime to 24 hour) IS BETWEEN(OpenTime,CloseTime)) AS BIT;

Если вам нужно хранить пограничные случаи, то просто имейте 365 записей, по одной в день... на самом деле это не так уж много в общей схеме вещей, поместите индекс в столбец дня и столбец businessId.

Не забудьте сохранить часовой пояс предприятия в отдельной таблице (нормализовать!), и выполнить преобразование между вашим временем и этим, прежде чем делать эти сравнения.

person FlySwat    schedule 26.09.2008

Я думаю, что лично я выбрал бы время начала + время окончания, так как это сделало бы все более гибким. Хороший вопрос: какова вероятность того, что размер блока изменится в определенный момент? Затем выберите решение, которое лучше всего подходит для вашей ситуации (если оно может измениться, я бы определенно выбрал временные промежутки).

Вы можете сохранить их как временной интервал и использовать сегменты в своем приложении. Таким образом, вы можете легко вводить данные с помощью блоков, сохраняя при этом гибкость для изменений в вашем хранилище данных.

person Erik van Brakel    schedule 26.09.2008

Чтобы добавить к тому, что сказал Джонатан Холланд , Я бы разрешил несколько записей в один и тот же день.

Я бы также разрешил десятичное время или другой столбец для минут.

Почему? во многих ресторанах и некоторых компаниях, а также во многих компаниях по всему миру есть обеденный и/или дневной перерыв. Кроме того, многие рестораны (2 из которых я знаю рядом с моим домом) закрываются в нечетное время, отличное от 15. Один закрывается в 21:40 по воскресеньям, а другой закрывается в 1:40.

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

Возможно, что можно сделать, это открыть дату/время, закрыть дату и время, например:

businessID  | datetime              | type
==========================================
        1     10/1/2008 10:30:00 AM    1
        1     10/1/2008 02:45:00 PM    0
        1     10/1/2008 05:15:00 PM    1
        1     10/2/2008 02:00:00 AM    0
        1     10/2/2008 10:30:00 AM    1

и т. д. (тип: 1 открытый и 0 закрытый)

И рассчитать все дни на ближайшие 1-2 года на 1-2 года вперед. Обратите внимание, что у вас будет только 3 столбца: int, дата/время/бит, поэтому потребление данных должно быть минимальным.

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

Он также заботится о пересечении полуночи, а также о преобразованиях 12/24 часов.

Он также не зависит от часового пояса. Если вы сохраняете время начала и продолжительность, когда вы вычисляете время окончания, будет ли ваша машина давать вам скорректированное время TZ? Это то, что вы хотите? Больше кода.

что касается запроса статуса открытия-закрытия: запросите соответствующую дату-время,

select top 1 type from thehours where datetimefield<=somedatetime and businessID = somebusinessid order by datetime desc

затем посмотрите на «тип». если 1 - открыто, если 0 - закрыто.

PS: я был в рознице в течение 10 лет. Так что я знаком с проблемами сумасшедших часов малого бизнеса.

person Christopher Mahan    schedule 26.09.2008

Хорошо, я добавлю это, потому что оно того стоит.

Мне нужно уладить довольно много дел.

  • Быстрый / производительный запрос
  • Любые приращения времени, 21:01, 12:14 и т. д.
  • Международный (?) - не уверен, что это проблема даже с часовыми поясами, по крайней мере, в моем случае, но кто-то более сведущий здесь, не стесняйтесь вмешиваться
  • Открыто — Закрытие до следующего дня (открытие в полдень, закрытие в 2:00)
  • Несколько промежутков времени / день
  • Возможность переопределить определенные дни (праздники, что угодно)
  • Возможность повторяющихся переопределений
  • Возможность запрашивать любой момент времени и открывать бизнес (сейчас, будущее время, прошлое время)
  • Возможность легко исключить результаты закрытия предприятий в ближайшее время (отфильтруйте предприятия, закрывающиеся через 30 минут, вы не хотите, чтобы ваши пользователи были «тем парнем, который появляется за 5 минут до закрытия в индустрии продуктов питания / напитков»).

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

Вот что я предлагаю для алгоритма и структуры.

Мы должны сделать некоторые конкретные предположения по всему миру, в любом месте и в любое время: в неделе 7 дней. В сутках 1440 минут. Существует конечное число возможных комбинаций минут открытия/закрытия.

Не конкретные, но достойные предположения: многие перестановки открытых/закрытых минут будут распределяться между предприятиями, уменьшая общее количество фактически хранимых перестановок. Было время в моей жизни, когда я мог легко рассчитать фактические возможные комбинации этого подхода, но если кто-то может помочь / думает, что это будет полезно, это было бы здорово.

Я предлагаю 3 таблицы: Прежде чем вы перестанете читать, учтите, что в реальном мире 2 из этих таблиц будут достаточно аккуратно кэшированы. Этот подход подойдет не всем из-за огромной сложности кода, необходимого для интерпретации пользовательского интерфейса в модели данных и обратно, если это необходимо. Ваш пробег и потребности могут отличаться. Это попытка разумного решения на уровне предприятия, что бы это ни значило.

Таблица часов работы

ID | ОТКРЫТО (минуты дня) | ЗАКРЫТЬ (минуты дня)


1 | 360 | 1020 (пример: с 9:00 до 17:00)

2 | 365 | 1021 (пример: пограничный регистр 9:05 - 17:01 (чудаки))

и т.п.

HoursOfOperations не волнует, какие дни, просто открытые и закрытые и уникальность. На комбинацию открытия/закрытия может быть только один вход. Теперь, в зависимости от вашей среды, либо вся эта таблица может быть кэширована, либо она может кэшироваться для текущего часа дня и т. д. В любом случае вам не нужно запрашивать эту таблицу для каждой операции. В зависимости от вашего решения для хранения я предполагаю, что каждый столбец в этой таблице проиндексирован для повышения производительности. С течением времени эта таблица, вероятно, имеет экспоненциально обратную вероятность INSERT(ов). Однако на самом деле работа с этой таблицей должна быть в основном внутрипроцессной операцией (ОЗУ).

Business2HoursКарта

Примечание. В моем примере я сохраняю «День» в виде поля/столбца с битовым флагом. Во многом это связано с моими потребностями и продвижением перечислений LINQ/Flags в C#. Ничто не мешает вам расширить это до 7-битных полей. Оба подхода должны быть относительно похожи как в логике хранения, так и в подходе к запросам.

Еще одно примечание: я не вступаю в семантический аргумент о том, что «каждой таблице нужен столбец идентификатора PK», пожалуйста, найдите для этого другой форум.

Идентификатор бизнеса | Идентификатор часов | День (или, если хотите, разделить на: BIT Monday, BIT вторник, ...)


1 | 1 | 1111111 (этот бизнес работает с 9 до 5 каждый день недели)

2 | 2 | 1111110 (эта компания открыта с 9:05 до 5:01 Пн-Сб (понедельник = день 1)

Причина, по которой это легко запросить, заключается в том, что мы всегда можем довольно легко определить MOTD (минуту дня), которая нам нужна. Если я хочу знать, что открыто завтра в 17:00, я беру все идентификаторы HoursOfOperations WHERE Close >= 1020. Если я не ищу временной диапазон, Open становится незначительным. Если вы не хотите показывать, что предприятия закрываются в ближайшие полчаса, просто соответствующим образом измените время прибытия (ищите 17:30 (10:50), а не 17:00 (10:20). Второй запрос, естественно, будет ' дайте мне все дела с HoursID IN (1, 2, 3, 4, 5) и т. д. Это, вероятно, должно поднять красный флаг, поскольку у этого подхода есть ограничения.Однако, если кто-то может ответить на фактический вопрос о перестановках выше, мы можем быть в состоянии опустить красный флаг.Учтите, что нам нужны только возможные перестановки на любой стороне уравнения одновременно, либо открытые, либо закрытые.

Учитывая, что у нас есть наша первая таблица в кэше, это быстрая операция. Вторая операция запрашивает эту потенциально большую таблицу строк, но мы ищем очень маленькие (SMALLINT) столбцы, которые, как мы надеемся, проиндексированы.

Теперь вы можете видеть сложность кода. В моем конкретном проекте я ориентируюсь в основном на бары, поэтому можно с уверенностью предположить, что у меня будет значительное количество предприятий с такими часами, как «11:00 - 2:00 (следующего дня)». Это действительно будет 2 записи как в таблице HoursOfOperations, так и в таблице Business2HoursMap. Например. бар, который открыт с 11:00 до 2:00, будет иметь 2 ссылки на таблицу HoursOfOperations 660–1440 (11:00–полночь) и 0–120 (полночь–2:00). Эти ссылки будут отражены в фактических днях в таблице Business2HoursMap как 2 записи в нашем упрощенном случае, 1 запись = ссылка на все дни № 1, еще одна ссылка на все дни № 2. Надеюсь, это имеет смысл, это был долгий день.

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

Идентификатор часов | Идентификатор бизнеса | день | Месяц | Год

1 | 2 | 1 | 1 | НУЛЕВОЙ

Это, безусловно, может стать более сложным, если вам нужно что-то вроде «каждый второй вторник эта компания ходит на рыбалку на 4 часа». Однако то, что это позволит нам сделать довольно легко, это разрешить 1 - переопределения, 2 - разумные повторяющиеся переопределения. НАПРИМЕР. если год равен NULL, то каждый год в день Нового года этот странный бар открыт с 9:00 до 17:00, что соответствует приведенным выше примерам данных. т.е. - Если указан год, то это только для 2013 года. Если месяц равен нулю, это каждый первый день месяца. Опять же, это не будет обрабатывать каждый сценарий планирования только по столбцам NULL, но теоретически вы можете справиться практически с чем угодно, полагаясь при необходимости на длинную последовательность абсолютных дат.

Опять же, я бы кэшировал эту таблицу по скользящему дню. Я просто не могу реально представить, что строки для этой таблицы в моментальном снимке за один день будут очень большими, по крайней мере, для моих нужд. Я бы сначала проверил эту таблицу, так как она переопределена, и сохранил бы запрос к гораздо большей таблице Business2HoursMap на стороне хранилища.

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

person Dave Jellison    schedule 19.11.2013
comment
Ух ты! Отличный анализ. Проект давно закончился, у меня больше нет кода, но я сделал простой анализатор строк (в основном разбиение и регулярные выражения), который мог читать искусственную строку, такую ​​​​как M-F 9-5, Sat 10-2, Sun 10-1. взято с сайта ресторана. Он преобразовал его во временные диапазоны с помощью записи (DayOfWeek). Я думаю, что мы просто проигнорировали особые случаи :) [показали пользователю фактическую текстовую строку, которая была у ресторана, в дополнение к нашему прогнозу того, был ли он открыт... Я точно не помню.] - person Kalid; 20.11.2013
comment
Спасибо любопытно! Я подумал, что ваш первоначальный вопрос уже решен, но хороший вопрос об алгоритме живет вечно в SO;) Надеюсь, это поможет кому-то еще, кому может понадобиться обрабатывать разные случаи, которые мне нужны. Вот почему я также сделал несколько заявлений об отказе от ответственности, мой подход не прост, и это не лучший ответ для всех в каждом сценарии. Я думаю, что это законный подход для людей, нуждающихся в комплексном решении, которое хорошо масштабируется. На самом деле я почти закончил писать код для него и опубликую его. Надеюсь, через год я смогу прокомментировать, насколько хорошо он масштабируется для запросов. - person Dave Jellison; 20.11.2013

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

Любая другая система (например, диапазоны) будет очень раздражать, когда вы пересечете границу полуночи.

Что касается того, как вы их храните, битовые поля С++, вероятно, будут лучшими. В большинстве других языков и массив может быть лучше (много потраченного впустую места, но он будет работать быстрее и его будет легче понять).

person Bill K    schedule 26.09.2008
comment
Тривиально поддерживать ввод данных через сегменты, сохраняя при этом данные внутри в виде пар «начало/длительность». Нет причин выбирать внутреннее представление в зависимости от того, что лучше для пользовательского интерфейса. - person Derek Park; 27.09.2008
comment
Полностью согласен. Я дал два ответа. Лучший способ сохранить его - в 15-минутных периодах времени, потому что тест тривиален ... Вам не нужно иметь дело с потенциальными совпадениями, и проблемы с полуночными часами не могут возникнуть. Часть с графическим интерфейсом просто говорила, как лучше всего представить пользователю любой набор значений периода. - person Bill K; 27.09.2008

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

Существует так много исключений — и на регулярной основе (например, снежные дни, нерегулярные праздники, такие как Пасха, Страстная пятница), что, если ожидается, что это надежное представление реальности (в отличие от хорошего предположения), вам понадобится чтобы решить эту проблему довольно скоро в архитектуре.

person Cade Roux    schedule 26.09.2008

Как насчет такого:

Таблица часов работы магазина

Business_id (int)
Start_Time (time)
End_Time (time)
Condition varchar/string
Open bit

«Условие» — это лямбда-выражение (текст предложения «где»). Создайте запрос динамически. Таким образом, для конкретного бизнеса вы выбираете все время открытия/закрытия.

Let Query1 = select count(open) from store_hours where @t between start_time and end_time and open  = true and business_id = @id and (.. dynamically built expression)

Let Query2 = select count(closed) from store_hours where @t between start_time and end_time and open = false and business_id = @id and (.. dynamically built expression)

Итак, в конце вы хотите что-то вроде:

select cast(Query1 as bit) & ~cast(Query2 as bit)

Если результат последнего запроса равен 1, то магазин открыт в момент времени t, иначе он закрыт.

Теперь вам просто нужен дружественный интерфейс, который может генерировать для вас предложения where (лямбда-выражения).

Единственный другой угловой случай, о котором я могу думать, это то, что происходит, если магазин открыт, скажем, с 7:00 до 2:00 в один день, но закрывается в 23:00 в следующий день. Ваша система должна быть в состоянии справиться с этим, разумно разделив время между двумя днями.

person Rodrick Chapman    schedule 27.09.2008

Конечно, здесь не нужно экономить память, но, возможно, нужен чистый и понятный код. ИМХО "поковыряться" - это не выход.

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

Концепция: присваивайте порядковый номер каждому 15-минутному блоку, начиная, скажем, с полуночи воскресенья.

Инициализация: вставьте в набор порядковый номер каждого 15-минутного блока, когда вы открыты. (Предполагая, что вы открыты меньше часов, чем закрыты.)

Использование: вычтите из интересного времени в минутах полночь предыдущего воскресенья и разделите на 15. Если это число присутствует в наборе, вы открыты.

person ravenspoint    schedule 26.09.2008
comment
Это в точности то же самое, что и подход с набором битов, за исключением того, что требования к памяти намного выше, а реализация сложнее. - person Derek Park; 27.09.2008
comment
Это просто немного возиться с меньшей элегантностью. - person Wedge; 27.09.2008
comment
Нет такой вещи, как элегантное вращение бит! - person ravenspoint; 27.09.2008