Biblioteca chrono - Ejemplo de uso: contando los días laborables en un período

En este post analizaremos el empleo de la biblioteca estándar <chrono> [1] para la codificación de un algoritmo sencillo que permita determinar el número de días laborables en un período temporal dado. En nuestros ejemplos haremos uso, entre otras, de la función std::chrono::parse [2] incluida en C++20 y que cuenta con una implementación experimental en el compilador GCC 14.

A modo de caso concreto de estudio, nos centraremos en el ámbito educativo a fin de computar los días lectivos dentro de cualquier intervalo cerrado [sd_0, sd_1], siendo sd_0 y sd_1 dos fechas enmarcadas en un curso académico que cumplan sd_0 ≤ sd_1. Como días lectivos asumiremos todos aquéllos de la semana exceptuando sábados, domingos y festivos.

Como primer paso, definamos un concepto Holiday_list destinado a restringir el tipo de contenedor en que poder registrar los días festivos:

#include <algorithm> #include <chrono> #include <ranges> template<typename T> concept Holiday_list = std::ranges::contiguous_range<T>                   and std::same_as<std::ranges::range_value_t<T>, std::chrono::sys_days>;

A fin de garantizar una mayor localidad, requerimos que el listado de festivos constituya un rango contiguo [3]. Asimismo, sus elementos subyacentes deben ser de tipo std::chrono::sys_days, es decir, puntos temporales con una resolución de un día que almacenan recuentos de días desde la época del system_clock [4]. En concreto, std::chrono::sys_days viene establecido como un alias para la clase std::chrono::time_point<std::chrono::system_clock, std::chrono::days>. En la práctica, trabajaremos con un contenedor std::array<> o std::vector<> según el desglose de fechas se establezca estática o dinámicamente, respectivamente.

A continuación, definimos un tipo Period cuyos objetos representen distintos períodos de tiempo, cada uno de los cuales quede limitado por su fecha de inicio y, de forma inclusiva, por su fecha final:

struct Period {     std::chrono::sys_days sd_0, // fecha inicial del período                          sd_1; // fecha final del período (incluida) };

La codificación del algoritmo que, dado un período y un listado de festivos, determina el número de días lectivos dentro del primero, resulta directa:

   template<Holiday_list T>    [[nodiscard]]    constexpr auto num_school_days(Period const& period, T const& holidays) noexcept -> std::int64_t    // pre ((period.sd_0 <= period.sd_1) and std::ranges::is_sorted(holidays) == true)    // post (r: r >= 0)    {       namespace stdc = std::chrono;       auto const& [sd_0, sd_1] = period;                                                         // (A)       auto const num_total_days = (sd_1 - sd_0).count() + 1;                                     // (B)       auto const num_weekend_days = /* IILE */ [&, wd_0 = stdc::weekday{sd_0},                                                    wd_1 = stdc::weekday{sd_1}] {                 // (C)          auto res = ((num_total_days + wd_0.c_encoding()) / 7)*2;                                // (D)          if (wd_0 == stdc::Sunday) { ++res; }                                                    // (E)          if (wd_1 == stdc::Saturday) { --res; }                                                  // (F)          return res;       }();       auto const num_holidays = /* IILE */ [&]{                                                  // (G)          // iterador a primer elemento mayor o igual que sd_0:          auto const it_1 = std::ranges::lower_bound(holidays, sd_0);                             // (H)          // iterador a primer elemento mayor estricto que sd_1:          auto const it_2 = std::ranges::upper_bound(it_1, std::ranges::end(holidays), sd_1);     // (I)          return std::ranges::distance(it_1, it_2);       }();       return num_total_days - num_weekend_days - num_holidays;                                   // (J)    }

Asumimos aquí, como precondiciones, el hecho de que la fecha de inicio del período preceda o sea idéntica a la fecha final, así como que el listado de días festivos std::chrono::sys_days se encuentre ordenado de menor a mayor según la política por defecto std::ranges::less. Como postcondición, anotamos el hecho de que el número calculado de días lectivos sea mayor o igual que cero. Tanto las precondiciones como la postcondición son facilitadas como meros comentarios, a la espera de la futura inclusión de los contratos en el estándar del lenguaje --con una sintaxis idéntica o similar a señalada en el código--, potencialmente en C++26 [5].

En la línea (A), el período period es desestructurado en sus componentes sd_0 (fecha inicial) y sd_1 (fecha final) para una mayor facilidad de codificación. El número total de días num_total_days  entre ambas fechas (igual a la unidad en caso de días coincidentes), sean lectivos o no, es determinado en (B) mediante una sencilla operación aritmética permitida por la clase std::chrono::sys_days. En (C) determinamos el número de sábados y/o domingos num_weekend_days contenidos en el período, según el método sencillo descrito en [6], mediante una invocación inmediata de una expresión lambda (IILE en sus siglas en Inglés; véase este post previo para más detalles). En particular, obsérvese que los puntos temporales sd_0 y sd_1 son convertidos en objetos std::chrono::weekday a fin de poder determinar de forma directa y eficiente los días de la semana (lunes, martes, etc.) en que éstos recaen. La línea (D) determina el número de sábados y domingos contenidos en el período sin más que multiplicar por dos el número de sábados dentro del mismo, con la salvedad de que su día inicial recaiga en domingo o el final en sábado, en cuyo caso el cómputo debe ser corregido en una unidad (líneas (E) y (F)). Nótese el empleo de la llamada a la función c_encoding() para determinar el número de día de la semana al que corresponde la fecha inicial del período (un valor en el rango [1..6] en caso de lunes a sábado, 0 para domingo).

El número de días festivos num_holidays contenidos en el período [sd_0, sd_1] es determinado mediante la invocación de una segunda lambda (G), la cual realiza tal conteo mediante búsqueda binaria aprovechando que (i) sd_0 ≤ sd_1 y (ii) la lista de festivos se asume ya ordenada de menor a mayor. Tanto el algoritmo std::ranges::lower_bound [7] como std::ranges::upper_bound [8], empleados en las líneas (H) e (I), poseen complejidades logarítmicas en este caso. En particular, std::ranges::lower_bound se encarga de obtener un iterador al primer festivo (si existe) mayor o igual que la fecha de inicio sd_0 del período, siendo igual a std::ranges::end(holidays) en caso de no haber ninguno. std::ranges::upper_bound, por su parte, retorna un iterador al primer festivo estrictamente posterior a la fecha fin sd_1 (de haberlo), siendo igual a std::ranges::end(holidays) en caso de no existir ninguno.

El número de días lectivos viene entonces dado por la diferencia num_total_days - num_weekend_days - num_holidays en la línea de retorno (J).

El siguiente código ejemplifica el modo de uso del algoritmo:

   namespace stdc = std::chrono;    constexpr auto holidays = std::array<stdc::sys_days, 22>{       stdc::November/1/2024, stdc::December/6/2024, stdc::December/23/2024, stdc::December/24/2024,       stdc::December/25/2024, stdc::December/26/2024, stdc::December/27/2024, stdc::December/30/2024,       stdc::December/31/2024, stdc::January/1/2025, stdc::January/2/2025, stdc::January/3/2025,       stdc::January/6/2025, stdc::April/14/2025, stdc::April/15/2025, stdc::April/16/2025,       stdc::April/17/2025, stdc::April/18/2025, stdc::May/1/2025, stdc::May/2/2025, stdc::May/15/2025,       stdc::July/25/2025    };    auto period = Period{};    auto str = std::string{};    std::print("Fecha inicio (dd/mm/yy): ");    std::getline(std::cin, str);    auto iss = std::istringstream{str};    iss >> stdc::parse("%d/%m/%y", period.sd_0);    std::print("Fecha fin (dd/mm/yy): ");    std::getline(std::cin, str);    iss = std::istringstream{str};    iss >> stdc::parse("%d/%m/%y", period.sd_1);    std::println("Días lectivos en período: {}", num_school_days(period, holidays));

Así, tomando por ejemplo el curso académico 2024-2025 (comprendido entre el 1 de septiembre de 2024 y el 31 de julio de 2025) y los días festivos señalados en el array holidays, el usuario puede probar a introducir períodos dentro del curso y obtener los días lectivos contenidos en el mismo. Obsérvese que, en el ejemplo, holidays incluye todos los días no-lectivos distintos a sábado o domingo incluyendo períodos vacacionales como Navidades o Semana Santa. A fin de facilitar su comprensión, el código carece de control de errores (producidos, por ejemplo, en las operaciones de parseo std::chrono::parse) o de fechas (para comprobar que un período sea válido y se enmarque dentro de los límites del curso académico). Un posible output del programa sería, así:



Referencias bibliográficas:
  1. cppreference - <chrono> standard library - https://en.cppreference.com/w/cpp/header/chrono
  2. cppreference - std::chrono::parse - https://en.cppreference.com/w/cpp/chrono/parse
  3. cppreference - std::ranges::contiguous_range - https://en.cppreference.com/w/cpp/ranges/contiguous_range
  4. cppreference - std::chrono::system_clock - https://en.cppreference.com/w/cpp/chrono/system_clock
  5. Contracts proposal for C++ - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2900r3.pdf
  6. StackOverflow - https://stackoverflow.com/questions/22270691/get-the-number-of-trading-days-in-between-two-days
  7. cppreference - std::ranges::lower_bound - https://en.cppreference.com/w/cpp/algorithm/ranges/lower_bound
  8. cppreference - std::ranges::upper_bound - https://en.cppreference.com/w/cpp/algorithm/ranges/upper_bound

No hay comentarios:

Publicar un comentario