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
|