LCOV - code coverage report
Current view: top level - ezlibs - ezDate.hpp (source / functions) Coverage Total Hit
Test: Coverage (llvm-cov → lcov → genhtml) Lines: 97.4 % 154 150
Test Date: 2025-09-16 22:55:37 Functions: 100.0 % 22 22
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 84.4 % 64 54

             Branch data     Line data    Source code
       1                 :             : #pragma once
       2                 :             : 
       3                 :             : /*
       4                 :             : MIT License
       5                 :             : 
       6                 :             : Copyright (c) 2014-2025 Stephane Cuillerdier (aka aiekick)
       7                 :             : 
       8                 :             : Permission is hereby granted, free of charge, to any person obtaining a copy
       9                 :             : of this software and associated documentation files (the "Software"), to deal
      10                 :             : in the Software without restriction, including without limitation the rights
      11                 :             : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      12                 :             : copies of the Software, and to permit persons to whom the Software is
      13                 :             : furnished to do so, subject to the following conditions:
      14                 :             : 
      15                 :             : The above copyright notice and this permission notice shall be included in all
      16                 :             : copies or substantial portions of the Software.
      17                 :             : 
      18                 :             : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      19                 :             : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      20                 :             : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      21                 :             : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      22                 :             : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      23                 :             : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
      24                 :             : SOFTWARE.
      25                 :             : */
      26                 :             : 
      27                 :             : // ezDate is part of the ezLibs project : https://github.com/aiekick/ezLibs.git
      28                 :             : 
      29                 :             : #include <string>
      30                 :             : #include <sstream>
      31                 :             : #include <cstdint>
      32                 :             : #include <iomanip>
      33                 :             : #include <cstdlib>
      34                 :             : 
      35                 :             : /**
      36                 :             :  * @file EzDate.hpp
      37                 :             :  * @brief Simple civil date utilities (YYYY-MM-DD), header-only.
      38                 :             :  *
      39                 :             :  * Design:
      40                 :             :  * - `addDays(date, offset)` performs generic civil date arithmetic using a day count since 1970-01-01.
      41                 :             :  * - `dayOfMonthToDate(day, firstOfMonth)` interprets a SQL-like "day index in month":
      42                 :             :  *      * day >= 1 => day-of-month (1..31) relative to the 1st
      43                 :             :  *      * day <  1 => negative offset from the 1st (e.g., -2 => two days before the 1st)
      44                 :             :  *      * day == 0 => invalid in this model (returns firstOfMonth as fallback)
      45                 :             :  *
      46                 :             :  * Leap years and month lengths are handled by Gregorian rules.
      47                 :             :  */
      48                 :             : 
      49                 :             : namespace ez {
      50                 :             : namespace date {
      51                 :             : /**
      52                 :             :  * @brief Parse a "YYYY-MM-DD" date string into numeric year, month, and day.
      53                 :             :  *
      54                 :             :  * @param s Date string to parse.
      55                 :             :  * @param y [out] Parsed year.
      56                 :             :  * @param m [out] Parsed month in [1..12].
      57                 :             :  * @param d [out] Parsed day in [1..31].
      58                 :             :  * @return true if parsing succeeds and fields look valid; false otherwise.
      59                 :             :  */
      60                 :          80 : inline bool parseYmd(const std::string& s, int32_t& y, int32_t& m, int32_t& d) {
      61 [ +  + ][ -  + ]:          80 :     if (s.size() != 10 || s[4] != '-' || s[7] != '-')
                 [ -  + ]
      62                 :           2 :         return false;
      63                 :          78 :     y = std::atoi(s.substr(0, 4).c_str());
      64                 :          78 :     m = std::atoi(s.substr(5, 2).c_str());
      65                 :          78 :     d = std::atoi(s.substr(8, 2).c_str());
      66 [ +  - ][ +  + ]:          78 :     return (y != 0 && m >= 1 && m <= 12 && d >= 1 && d <= 31);
         [ +  + ][ +  + ]
                 [ +  - ]
      67                 :          80 : }
      68                 :             : 
      69                 :             : /**
      70                 :             :  * @brief Check if a given year is a leap year (Gregorian rules).
      71                 :             :  *
      72                 :             :  * @param y Year.
      73                 :             :  * @return true if leap year; false otherwise.
      74                 :             :  */
      75                 :        3439 : inline bool isLeap(int32_t y) {
      76 [ +  + ][ +  + ]:        3439 :     return (y % 4 == 0) && ((y % 100 != 0) || (y % 400 == 0));
                 [ +  + ]
      77                 :        3439 : }
      78                 :             : 
      79                 :             : /**
      80                 :             :  * @brief Number of days in a given month of a given year.
      81                 :             :  *
      82                 :             :  * @param y Year.
      83                 :             :  * @param m Month in [1..12].
      84                 :             :  * @return Number of days in that month (28, 29, 30, or 31).
      85                 :             :  */
      86                 :         263 : inline int32_t monthDays(int32_t y, int32_t m) {
      87                 :         263 :     static const int32_t md[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
      88                 :         263 :     int32_t d = md[m - 1];
      89 [ +  + ][ +  + ]:         263 :     if (m == 2 && isLeap(y))
      90                 :          16 :         d = 29;
      91                 :         263 :     return d;
      92                 :         263 : }
      93                 :             : 
      94                 :             : /**
      95                 :             :  * @brief Convert a date to the number of civil days since 1970-01-01.
      96                 :             :  *
      97                 :             :  * This is a simple civil-day count (not time-zone aware and not tied to Unix time).
      98                 :             :  *
      99                 :             :  * @param Y Year.
     100                 :             :  * @param M Month in [1..12].
     101                 :             :  * @param D Day in [1..31].
     102                 :             :  * @return Days since 1970-01-01 (can be negative for dates before 1970-01-01).
     103                 :             :  */
     104                 :          55 : inline int32_t ymdToDays(int32_t Y, int32_t M, int32_t D) {
     105                 :          55 :     int32_t y = Y, m = M, d = D;
     106                 :          55 :     int32_t n = 0;
     107         [ +  + ]:        1940 :     for (int32_t yy = 1970; yy < y; ++yy)
     108         [ +  + ]:        1885 :         n += 365 + (isLeap(yy) ? 1 : 0);
     109         [ +  + ]:         176 :     for (int32_t mm = 1; mm < m; ++mm)
     110                 :         121 :         n += monthDays(y, mm);
     111                 :          55 :     n += d - 1;
     112                 :          55 :     return n;
     113                 :          55 : }
     114                 :             : 
     115                 :             : /**
     116                 :             :  * @brief Convert a civil day count since 1970-01-01 back to a date.
     117                 :             :  *
     118                 :             :  * @param days Day count since 1970-01-01.
     119                 :             :  * @param Y [out] Year.
     120                 :             :  * @param M [out] Month in [1..12].
     121                 :             :  * @param D [out] Day in [1..31].
     122                 :             :  */
     123                 :          39 : inline void daysToYmd(int32_t days, int32_t& Y, int32_t& M, int32_t& D) {
     124                 :          39 :     int32_t y = 1970;
     125                 :        1494 :     while (true) {
     126         [ +  + ]:        1494 :         int32_t len = 365 + (isLeap(y) ? 1 : 0);
     127         [ +  + ]:        1494 :         if (days >= len) {
     128                 :        1455 :             days -= len;
     129                 :        1455 :             ++y;
     130                 :        1455 :         } else
     131                 :          39 :             break;
     132                 :        1494 :     }
     133                 :          39 :     int32_t m = 1;
     134                 :         129 :     while (true) {
     135                 :         129 :         int32_t len = monthDays(y, m);
     136         [ +  + ]:         129 :         if (days >= len) {
     137                 :          90 :             days -= len;
     138                 :          90 :             ++m;
     139                 :          90 :         } else
     140                 :          39 :             break;
     141                 :         129 :     }
     142                 :          39 :     int32_t d = days + 1;
     143                 :          39 :     Y = y;
     144                 :          39 :     M = m;
     145                 :          39 :     D = d;
     146                 :          39 : }
     147                 :             : 
     148                 :             : /**
     149                 :             :  * @brief Add a signed offset (in days) to a "YYYY-MM-DD" date (generic arithmetic).
     150                 :             :  *
     151                 :             :  * @param s Start date, "YYYY-MM-DD".
     152                 :             :  * @param off Day offset (positive or negative).
     153                 :             :  * @return New date as "YYYY-MM-DD". If parsing fails, returns the input string.
     154                 :             :  */
     155                 :          18 : inline std::string addDays(const std::string& s, int32_t off) {
     156                 :          18 :     int32_t y, m, d;
     157         [ -  + ]:          18 :     if (!parseYmd(s, y, m, d))
     158                 :           0 :         return s;
     159                 :          18 :     int32_t base = ymdToDays(y, m, d);
     160                 :          18 :     int32_t tgt = base + off;
     161                 :          18 :     int32_t Y, M, D;
     162                 :          18 :     daysToYmd(tgt, Y, M, D);
     163                 :          18 :     std::ostringstream oss;
     164                 :          18 :     oss << std::setfill('0') << std::setw(4) << Y << "-" << std::setw(2) << M << "-" << std::setw(2) << D;
     165                 :          18 :     return oss.str();
     166                 :          18 : }
     167                 :             : 
     168                 :             : /**
     169                 :             :  * @brief Difference in days between two dates.
     170                 :             :  *
     171                 :             :  * @param a Date A, "YYYY-MM-DD".
     172                 :             :  * @param b Date B, "YYYY-MM-DD".
     173                 :             :  * @return a - b in days (positive if a > b, negative if a < b).
     174                 :             :  */
     175                 :           8 : inline int32_t diffDays(const std::string& a, const std::string& b) {
     176                 :           8 :     int32_t ya, ma, da, yb, mb, db;
     177 [ -  + ][ -  + ]:           8 :     if (!parseYmd(a, ya, ma, da) || !parseYmd(b, yb, mb, db))
     178                 :           0 :         return 0;
     179                 :           8 :     return ymdToDays(ya, ma, da) - ymdToDays(yb, mb, db);
     180                 :           8 : }
     181                 :             : 
     182                 :             : /**
     183                 :             :  * @brief First day of the month for a given date.
     184                 :             :  *
     185                 :             :  * @param s Any date, "YYYY-MM-DD".
     186                 :             :  * @return "YYYY-MM-01" (or the input if parsing fails).
     187                 :             :  */
     188                 :           1 : inline std::string startOfMonth(const std::string& s) {
     189                 :           1 :     int32_t y, m, d;
     190         [ -  + ]:           1 :     if (!parseYmd(s, y, m, d))
     191                 :           0 :         return s;
     192                 :           1 :     std::ostringstream oss;
     193                 :           1 :     oss << std::setfill('0') << std::setw(4) << y << "-" << std::setw(2) << m << "-01";
     194                 :           1 :     return oss.str();
     195                 :           1 : }
     196                 :             : 
     197                 :             : /**
     198                 :             :  * @brief Last day of the month for a given date.
     199                 :             :  *
     200                 :             :  * Leap years are handled for February.
     201                 :             :  *
     202                 :             :  * @param s Any date, "YYYY-MM-DD".
     203                 :             :  * @return "YYYY-MM-DD" with the month's last day (or the input if parsing fails).
     204                 :             :  */
     205                 :           3 : inline std::string endOfMonth(const std::string& s) {
     206                 :           3 :     int32_t y, m, d;
     207         [ -  + ]:           3 :     if (!parseYmd(s, y, m, d))
     208                 :           0 :         return s;
     209                 :           3 :     int32_t md = monthDays(y, m);
     210                 :           3 :     std::ostringstream oss;
     211                 :           3 :     oss << std::setfill('0') << std::setw(4) << y << "-" << std::setw(2) << m << "-" << std::setw(2) << md;
     212                 :           3 :     return oss.str();
     213                 :           3 : }
     214                 :             : 
     215                 :             : /**
     216                 :             :  * @brief Extract a "YYYY-MM" year-month key from a date string.
     217                 :             :  *
     218                 :             :  * @param s Date "YYYY-MM-DD" (or at least "YYYY-MM").
     219                 :             :  * @return "YYYY-MM" (or s if the string is shorter than 7 chars).
     220                 :             :  */
     221                 :           3 : inline std::string ymKey(const std::string& s) {
     222         [ +  + ]:           3 :     return (s.size() >= 7) ? s.substr(0, 7) : s;
     223                 :           3 : }
     224                 :             : 
     225                 :             : /**
     226                 :             :  * @brief Convert a "day index in month" to a concrete date (SQL-like interpretation).
     227                 :             :  *
     228                 :             :  * Rules:
     229                 :             :  *  - day >= 1 : treat it as a day-of-month (1..31) relative to the 1st
     230                 :             :  *               => offset = (day - 1) from the first day of the month
     231                 :             :  *  - day <  1 : treat it as a negative offset from the 1st
     232                 :             :  *               => e.g., -2 means "two days before the 1st"
     233                 :             :  *  - day == 0 : invalid in this 1..31 model; returns firstOfMonth as fallback
     234                 :             :  *
     235                 :             :  * Examples for firstOfMonth = "2025-08-01":
     236                 :             :  *  - day =  1 -> "2025-08-01"
     237                 :             :  *  - day =  2 -> "2025-08-02"
     238                 :             :  *  - day = -1 -> "2025-07-31"
     239                 :             :  *  - day = -2 -> "2025-07-30"
     240                 :             :  *
     241                 :             :  * @param day SQL-like day index (1..31 or negative offset).
     242                 :             :  * @param firstOfMonth The date representing the 1st of the target month ("YYYY-MM-DD").
     243                 :             :  * @return Concrete "YYYY-MM-DD" date according to the rules above.
     244                 :             :  */
     245                 :          12 : inline std::string dayOfMonthToDate(int32_t day, const std::string& firstOfMonth) {
     246         [ +  + ]:          12 :     if (day == 0) {
     247                 :             :         // Invalid in a 1..31 day-of-month model; choose your policy here.
     248                 :             :         // For safety, return the 1st as a neutral fallback.
     249                 :           1 :         return firstOfMonth;
     250                 :           1 :     }
     251         [ +  + ]:          11 :     if (day >= 1) {
     252                 :           5 :         return addDays(firstOfMonth, day - 1);
     253                 :           6 :     } else {
     254                 :           6 :         return addDays(firstOfMonth, day);
     255                 :           6 :     }
     256                 :          11 : }
     257                 :             : 
     258                 :             : class Date {
     259                 :             : private:
     260                 :             :     int32_t m_year{};
     261                 :             :     int32_t m_month{};
     262                 :             :     int32_t m_day{};
     263                 :             :     bool m_valid{};
     264                 :             : 
     265                 :             : public:
     266                 :             :     Date() = default;
     267                 :          36 :     explicit Date(const std::string& vDate) {
     268                 :          36 :         m_valid = parseYmd(vDate, m_year, m_month, m_day);
     269                 :          36 :     }    
     270                 :           5 :     int32_t getYear() const { return m_year; }
     271                 :           5 :     int32_t getMonth() const { return m_month; }
     272                 :           5 :     int32_t getDay() const { return m_day; }
     273                 :           2 :     Date& setYear(const int32_t vYear) {
     274                 :           2 :         m_year = vYear;
     275                 :           2 :         return *this;
     276                 :           2 :     }
     277                 :           5 :     Date& setMonth(const int32_t vMonth) {
     278         [ +  + ]:           5 :         if (vMonth < 1) {
     279                 :           1 :             m_month = 1;
     280         [ +  + ]:           4 :         } else if (vMonth > 12) {
     281                 :           1 :             m_month = 12;
     282                 :           3 :         } else {
     283                 :           3 :             m_month = vMonth;
     284                 :           3 :         }
     285                 :           5 :         return *this;
     286                 :           5 :     }
     287                 :           6 :     Date& setDay(const int32_t vDay) {
     288                 :           6 :         const int32_t md = monthDays(m_year, m_month);  // 28/29/30/31 according to (Y,M)
     289         [ +  + ]:           6 :         if (vDay < 1) {
     290                 :           1 :             m_day = 1;
     291         [ +  + ]:           5 :         } else if (vDay > md) {
     292                 :           2 :             m_day = md;
     293                 :           3 :         } else {
     294                 :           3 :             m_day = vDay;
     295                 :           3 :         }
     296                 :           6 :         return *this;
     297                 :           6 :     }
     298                 :           3 :     Date& offsetYear(const int32_t vOffset) {
     299                 :           3 :         m_year += vOffset;
     300                 :           3 :         return *this;
     301                 :           3 :     }
     302                 :           6 :     Date& offsetMonth(const int32_t vOffset) {
     303                 :             :         // 1) Mois absolu 0-based (janvier=0, ..., décembre=11)
     304                 :           6 :         const int64_t totalMonths = int64_t(m_year) * 12 + (m_month - 1) + int64_t(vOffset);
     305                 :             :         // 2) Division "plancher" (floor) pour gérer les valeurs négatives correctement
     306         [ +  - ]:           6 :         const int64_t newYear = (totalMonths >= 0) ? (totalMonths / 12) : ((totalMonths - 11) / 12);
     307                 :             :         // 3) Reste 0..11 puis retour en 1..12
     308                 :           6 :         const int32_t newMonth = int32_t(totalMonths - newYear * 12) + 1;
     309                 :           6 :         m_year = int32_t(newYear);
     310                 :           6 :         m_month = newMonth;
     311                 :           6 :         return *this;
     312                 :           6 :     }
     313                 :          21 :     Date& offsetDay(const int32_t vOffset) {
     314                 :          21 :         int32_t base = ymdToDays(m_year, m_month, m_day);
     315                 :          21 :         int32_t tgt = base + vOffset;
     316                 :          21 :         daysToYmd(tgt, m_year, m_month, m_day);
     317                 :          21 :         return *this;
     318                 :          21 :     }
     319                 :          40 :     std::string getDate() const {
     320                 :          40 :         std::ostringstream oss;
     321                 :          40 :         oss << std::setfill('0') << std::setw(4) << m_year << "-" << std::setw(2) << m_month << "-" << std::setw(2) << m_day;
     322                 :          40 :         return oss.str();
     323                 :          40 :     }
     324                 :             : };
     325                 :             : 
     326                 :             : }  // namespace date
     327                 :             : }  // namespace ez
        

Generated by: LCOV version 2.0-1