Вначале было слово, и было два байта, и ничего больше не было. Но отделил Бог ноль от единицы, и понял, что это хорошо.  Потом, опуская некоторые незначительные события мироздания, была вот эта статья  от @novar.  Ну а еще некоторое время спустя вышел разбор задания из оригинальной статьи от @PsyHaSTe. И обожемой, как этот разбор мне понравился. Серьезно, @PsyHaSTe, я теперь твой подписчик. Пиши еще, статья восхитительная, всем рекомендую.  Однако, покрутив немного сам код решения, я понял, что есть ряд моментов, которые я бы сделал иначе (так бывает, зачастую есть более одного способа решить задачу). В частности, меня зацепил вот этот кусок.  Получается, что при ограничении на миллисекунды , мы будем хранить килобайт памяти на каждый объект расписания, просто чтобы итерироваться по миллисекундам.  Мой внутренний хомяк схватился за сердце. В упомянутых в статье библиотеках cron и Quartz проблема менее критична: в кроне точность расписания до минуты, а в кварце до секунды. Но у нас - тысячи миллисекунд. Ладно, допустим использование памяти можно сократить, используя BitSet, но потом, в расписании типа 0:0:0.999  мы действительно 1000 раз пройдемся по элементам битсета, чтобы узнать, какая следующая миллисекунда валидная. Но ведь мы сразу знаем: нам подходит только 999.    Ну а после я глянул Closest<T>, где код везде разный, но уж очень напоминает копипасту, и рука сама потянулась глянуть, что получится у самого: критикуешь - предлагай. Дан cron-подобый формат, включающий годы, месяца, дни, дни недели, часы, минуты, секунды, миллисекунды:yyyy.MM.dd w HH:mm:ss.fff Например, расписание "10 утра и 10 вечера с понедельника по пятницу" будет выглядеть так:*.*.* 1-5 10,22:0:0.000 Требуется написать структуру данных, которая конструируется из расписания, и умеет быстро По заданному моменту времени находить ближайший следующий момент времени, подходящий под это расписание.  По заданному моменту времени находить ближайший предыдущий момент времени, подходящий под это расписание.  Поначалу хотелось сделать все на C#, чтобы честное сравнение получилось, но потом передумал: С++ я знаю лучше (хотя С++ и "знаю" - вообще такое себе сочетание), да и pet-проекты надо писать на том, что любишь, даже если любовь эта больше походит на стокгольмский синдром.  Ну и, если совсем честно, на фоне монад от @PsyHaSTe мой С# будет выглядеть студенческой лабораторной. Поэтому включаем VS, новый проект на крестах, пристегнулись и погнали. Начнем не сверху, а снизу: как мы представляем себе обход условий валидности дней, часов и т.д. Для начала сделаем обход "тривиальных условий": условий без списка ( 10-20/3 - тривиальное, 1,5,8 - нетривиальное).  И будем рассматривать только обход вперед: обход назад добавим потом. Таких условий не так и много: константа, любое значение(данного типа),  диапазон, и последние два с заданным шагом. Самое главное: хочется иметь некий "генератор следующих значений". Чтобы мы шли по "последовательности правильных", вместо того, чтобы проходить по всем и проверять, правильное ли текущее значение.  Для тривиальных условий это понятно как сделать: если у нас ограничение 5, то мы генерируем следующее значение 5, а потом говорим,  что новых значений нет. Если есть ограничение 1-5, то мы отдаем 1, потом 2, ... , потом 5, а потом говорим, что числа кончились.  Плюс, нам надо уметь "инициализировать" подобные "генераторы" числом (чтобы можно было начать с 3-й секунды, даже если диапазон 1-5),  и сбросить на "начало" (если мы увеличиваем час, то генератор минуты должны сброситься на начало).  В принципе, для всех тривиальных условий это задача концептуально решаемая. Осталось решить, как это будет выглядеть в коде. Как известно, С++ программист за жизнь должен сделать 3 вещи: Построить велосипед Посадить зрение Написать итератор Со временем пытаешься сделать все максимально похожим на итераторы, поэтому вот какой получился интерфейс для нашей "последовательности": Изначально назывался ICondition, но не пережил рефакторинга.  Предполагается пользоваться такими последовательностями как-то так: В условии while (r) проверяем, есть ли валидное значение, если есть, то оператор "звездочка" *r вернет нам это значение,  а оператор инкремента ++r попробует получить следующее значение.  Сделаем пару реализаций: Все оказалось действительно довольно просто. Любое значение: мы просто сохраняем текущее значение и возвращаем его. При инициализации мы подразумеваем, что стартовое значение внутри диапазона, а валидация проводилась при парсинге (это допущение можно сделать в последовательности "любое значение", но нельзя в последовательности "диапазон": диапазон 10-20 можно инициализировать значением 5).  Мы перестаем выдавать новые значения, когда наше "текущее" значение вышло за границы дозволенного.  Сбросить на начало - просто поставить стартовое значение.  Параметры Begin и End - это параметры времени компиляции со всеми вытекающими: оптимизации где это возможно,  для их хранения не используются переменные (не для каждого экземпляра: где-то в коде они есть), и т.д. Фактически, AnyYear, AnyMonth, AnyDay - это три разных класса, просто один шаблон. При этом, этот шаблон не перекомпилируется: в заголовочном файле нет ни одной реализации(definition), только объявления (declaration),  а в cpp файле последние строчки - это указание скомпилировать класс именно с этими константами.  Такое возможно только потому, что мы заранее знаем все константы, с которыми будет использоваться этот класс.  Зато если мы где-то попробуем использовать, например Any<0, 17>, сборка упадет: он не найдет определение функций класса Any<0, 17>. Добавим тесты к нашей последовательности.  На самом деле, остальные тоже делаются легко, уточню два момента.  В последовательности "константа" мы храним флаг. Если мы инициализируем последовательность числом не большим нашей константы, флаг возводится (это означает, что существует "следующее значение последовательности", и мы его вернем).  Попытка получить следующий элемент последовательности всегда сбивает этот флаг.  Последовательности "с шагом" можно унаследовать от обычных последовательностей:  при движении вперед там достаточно поменять инкремент и инициализацию, чтобы при инициализации мы сразу попадали в "валидное состояние". Ну теперь когда у нас есть базовые последовательности, пришло время научить их работать вместе.  С этим нам поможет структура данных "куча" (heap).  Куча - это структура данных, которая позволяет иметь быстрый O(1) доступ к минимальному/максимальному содержащемуся элементу, и позволяет относительно быстро добавлять и убирать из нее элементы. "Окучивание" - создание кучи - делается за O(n). Полезная особенность реализации кучи в стандартной библиотеке С++: она работает в уже выделенном массиве, а значит не требует аллокаций/деаллокаций памяти.  Идея ее использования следующая: если у нас комбинация из нескольких разных последовательностей, давайте соберем их в кучу.  "Текущее" значение мы всегда сможем получить за O(1) из вершины кучи: нам ведь нужно самое маленькое из валидных, правда? Что нам делать при инкременте?  Давайте вытащим последовательность из кучи. Вызовем у нее инкремент, и если в ней еще остались значения, засунем обратно в кучу.  Не остались - не засунем.  Ну и понятно, что когда у нас куча опустеет, мы прошлись по всем значениям во всех наших последовательностях. Есть один тонкий момент: наши "последовательности" могут пересекаться. Это вполне нормально, например, 10-20,*/3. Но мы не хотим из нашей "объединенной" последовательности возвращать одинаковые значения. Поэтому мы  выкидываем из кучи не только "топовую" последовательность, но и все те, у которых такое же текущее значение.  Ну и просто для красоты: комбайнёр последовательности - это тоже последовательность. Если бы вначале я дал четкое  определение последовательности, то сейчас бы магическим образом показал, что комбайнёр удовлетворяет всем требованиям  последовательности, но математик во мне ленится в последнее время. Тем не менее, с точки зрения кода это значит, что мы можем унаследовать комбайнёра от того же интерфейса.  отдельный комментарий вот этой строчке: Она отфильтровывает последовательности в которых нет значений.  В результате мы можем сделать сразу валидную кучу.  Как это работает, покажем при помощи косвенной документации (также известной как тесты): Заметим: это универсальная последовательность,  которую можно использовать и для месяцев, и для часов, и для секунд, и даже для дней недели. Время собрать все воедино. Давайте на этом этапе предположим, что фильтра по дням недели нет, а в каждом месяце 31 день. (^◕.◕^) Это не шутка, мы потом допилим напильником итератор до нужной кондиции:  большие коммиты сложно пишутся, плохо читаются, плохо тестируются, и т.п.  Надо пытаться декомпозировать задачу на маленькие и относительно понятные кусочки.  Итак, глупенький итератор со своими странными представлениями об устройстве календаря. Как получить текущее значение понятно: просто берем значение из всех комбайнеров, склеиваем в тип даты. Проинициализировать - тут сложнее. Пока просто разберем на составляющие дату и  проинициализируем каждый комбайнер отдельно (это баг, так делать нельзя, но мы поправим это в части 5). Вопрос: как инкрементировать, и когда останавливаться?  А оказывается, тут все просто: мы инкрементируем миллисекунды. Удалось - следующее значение получено. Не удалось - сбрасываем последовательность на "стартовое значение" и пытаемся инкрементировать секунды.  И так далее, до года: год сбрасывать нельзя - если мы вышли за границы года, то новых значений не будет. О! А вот и условие остановки. Мммм. Похоже что имея комбинированные последовательности глупенький итератор создается легко. К этому моменту пришло осознание,  что C++20 присутствует на VS только в оооооочень усеченном режиме, и функций работы с датой решительно не хватает. Поэтому для хранения даты теперь объявлена своя структура, и появилась зависимость на boost (но только в одном файле (все так говорят, а потом....)). Итак, нам надо как-то решить проблему с 31 февраля и иже с ними. Просто генераторами это решить сложно: появляется зависимость между генераторами дней, месяцев и лет, а это плохо.  Вместо этого разделим инкремент на две части: инкремент времени и инкремент даты. Если удалось получить следующее время - все хорошо (мы уже в валидном дне), если нет: инкрементируем дату,  а потом проверяем, валидна ли она. Если нет - инкрементируем дату снова (да, мы пытались этого избежать,  но вы не понимаете, это другое). Мы действительно генерируем "кандидатов" нашими последовательностями, а потом фильтруем их на пригодность.  Получился некий гибрид подходов. Тут нам и пригодился буст: end_of_month_day. Теперь давайте разберемся с самой сложной частью: инициализацией.  Здесь я полностью согласен с @PsyHaSTe: иногда goto оправдан.  Например, когда нам надо выйти из блока вложенных циклов.  Правда у меня его использование выглядит немного не так как в предыдущей статье. Разберем код по блокам. Первый цикл сбрасывает все последовательности. Если, например, мы инициализировали часы числом 3, а у нас условие 5,6, то первое возможное значение часов будет 5. Значит минуты, секунды и др надо будет сбросить на "начальное" значение, потому что чем бы мы не пытались их инициализировать, мы уже на другом часу. Если же все последовательности предварительно сброшены - мы просто заканчиваем работу.  Второй блок пытается поочередно инициализировать все последовательности. Что может случиться на этом этапе? Мы можем получить не то текущее значение, которым инициализировали. Тогда дальше ничего не надо инициализировать: см выше.  Мы можем попасть в не валидную дату.  Мы можем попасть вне блока (инициализировать "10-20" числом "30").  В этом случае надо будет увеличить предыдущую последовательность  (условие ".10." при инициализации "2010.11.1" должно указывать на 2011 год) Третий блок исправляет ошибку из условия 3.  Последний блок исправляет ошибку из условия 2. Очень надеюсь, что понятно объяснил. Судя по тестам, оно даже работает.  Эти два фильтра вписываются в текущий итератор элементарно: мы просто дописываем два условия в функцию is_valid_date Да, мы опять фильтруем, но вы опять не понимаете, это опять другое. Теперь, когда мы полноценно решаем задачу в одну сторону, хочется получить то же самое, но в обратную сторону.  Базовые последовательности лучше просто написать новые: попытка их обобщить приведет только к усложнению кода,  а никак не наоборот. Кроме того, есть ряд важных отличий, для примера можно посмотреть различия между RangeStep и RRangeStep.  А вот комбайнёра можно и переиспользовать: ему все-равно, какие последовательности у него под капотом.  Важно только правильно построить кучу: в обычном комбайнёре мы берем последовательность с самым, маленьким текущем значением, а в обратном надо брать с самым большим.  Сделаем это опять шаблонами и во время компиляции.  А теперь итератор.  Ну, итератору тоже достаточно все-равно, ему надо только задать тип комбайнёра И все, обратный итератор готов. Ну, парсера не будет. На этом этапе я уже достаточно сильно подустал, да и не думаю, что смогу здесь сделать какую-то революцию. Берите библиотеки на свой вкус (я бы, наверное, попробовал PEGTL) и преобразуйте строки в последовательности итератора. Несмотря на отсутствие парсинга, замерить время работы самого итератора можно. Например, я повторил тесты из статьи @PsyHaSTe: pattern date P90 P95 P98 *.*.1 0:0:0  01-01-2001  700 ns; 705 ns;  770 ns; *.*.1 0:0:0 05-05-2080 539 ns; 542 ns; 662 ns; *.4.6,7 * *:*:*.1,2,3-5,10-20/3   01-01-2001 540 ns; 542 ns;  582 ns; *.4.6,7 * *:*:*.1,2,3-5,10-20/3   05-05-2080 581 ns; 615 ns; 794 ns; 2100.12.31 23:59:59.999   01-01-2001 384 ns; 612 ns; 649 ns; 2100.12.31 23:59:59.999   05-05-2080 352 ns;  353 ns;  431 ns; Те результаты не привожу, если интересно - сами посмотрите.  Только отдельно хочу заметить, что время работы нельзя сравнивать (!!) тупо "в лоб".  По крайней мере потому, что там дата парсится из строки, а здесь передается структурой.  Да и вообще, мы тут не таймингами мериться пришли, а подходами к решению задачи обмениваться.  Ну, в первую очередь, спасибо @PsyHaSTe и @novar за то, что эта статья оказалось написана. Надеюсь, кому-то было полезно, или по крайней мере интересно.  В статье хотел показать еще один из вариантов решения задачи, который не требует хранения "допустимых" секунд. Пусть в итоге не дописал парсер, но на мой взгляд, все-равно вышло довольно любопытно. При этом времени я потратил более 12 часов, что несколько больше, чем  предыдущие два автора. Зато побочным эффектом мы получили фичу, которая не подразумевалась в изначальном задании: после инициализации итератора, мы можем очень быстро и эффективно получить сразу N следующих последовательных дат.  Если будут какие-то конструктивные замечания/предложения - буду очень рад услышать.  Ссылка на гитовую репу тут.  Всем спасибо за внимание.