Branch data Line data Source code
1 : : /********************************************************************\
2 : : * gnc-option-date.cpp -- Relative Dates for options *
3 : : * Copyright (C) 2020 John Ralls <jralls@ceridwen.us> *
4 : : * *
5 : : * This program is free software; you can redistribute it and/or *
6 : : * modify it under the terms of the GNU General Public License as *
7 : : * published by the Free Software Foundation; either version 2 of *
8 : : * the License, or (at your option) any later version. *
9 : : * *
10 : : * This program is distributed in the hope that it will be useful, *
11 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 : : * GNU General Public License for more details. *
14 : : * *
15 : : * You should have received a copy of the GNU General Public License*
16 : : * along with this program; if not, contact: *
17 : : * *
18 : : * Free Software Foundation Voice: +1-617-542-5942 *
19 : : * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
20 : : * Boston, MA 02110-1301, USA gnu@gnu.org *
21 : : \********************************************************************/
22 : :
23 : : #include "gnc-option-date.hpp"
24 : : #include <array>
25 : : #include "gnc-datetime.hpp"
26 : : #include <gnc-prefs.h>
27 : : #include <iostream>
28 : : #include <cassert>
29 : : #include <algorithm>
30 : :
31 : : #include "gnc-accounting-period.h"
32 : :
33 : : #define N_(string) string //So that xgettext will find it
34 : :
35 : : enum RelativeDateType
36 : : {
37 : : ABSOLUTE,
38 : : LAST,
39 : : NEXT,
40 : : START,
41 : : END
42 : : };
43 : :
44 : : enum RelativeDateOffset
45 : : {
46 : : NONE,
47 : : WEEK,
48 : : MONTH,
49 : : QUARTER,
50 : : THREE,
51 : : SIX,
52 : : YEAR
53 : : };
54 : :
55 : : struct GncRelativeDate
56 : : {
57 : : RelativeDatePeriod m_period;
58 : : RelativeDateType m_type;
59 : : RelativeDateOffset m_offset;
60 : : const char* m_storage;
61 : : const char* m_display;
62 : : const char* m_description;
63 : : };
64 : :
65 : :
66 : : /* The fixed values and strings for date periods. Accessor functions will use
67 : : * the RelativeDatePeriod as an index so any changes need to be reflected in the
68 : : * RelativeDatePeriod enum class in gnc-option-date.hpp and vice-versa.
69 : : *
70 : : * The double curly braces are actually correct and required for a std::array
71 : : * initializer list.
72 : : */
73 : : static const std::array<GncRelativeDate, 31> reldates
74 : : {{
75 : : {
76 : : RelativeDatePeriod::TODAY,
77 : : RelativeDateType::LAST,
78 : : RelativeDateOffset::NONE,
79 : : "today",
80 : : N_("Today"),
81 : : N_("The current date.")
82 : : },
83 : : {
84 : : RelativeDatePeriod::ONE_WEEK_AGO,
85 : : RelativeDateType::LAST,
86 : : RelativeDateOffset::WEEK,
87 : : "one-week-ago",
88 : : N_("One Week Ago"),
89 : : N_("One Week Ago.")
90 : : },
91 : : {
92 : : RelativeDatePeriod::ONE_WEEK_AHEAD,
93 : : RelativeDateType::NEXT,
94 : : RelativeDateOffset::WEEK,
95 : : "one-week-ahead",
96 : : N_("One Week Ahead"),
97 : : N_("One Week Ahead.")
98 : : },
99 : : {
100 : : RelativeDatePeriod::ONE_MONTH_AGO,
101 : : RelativeDateType::LAST,
102 : : RelativeDateOffset::MONTH,
103 : : "one-month-ago",
104 : : N_("One Month Ago"),
105 : : N_("One Month Ago.")
106 : : },
107 : : {
108 : : RelativeDatePeriod::ONE_MONTH_AHEAD,
109 : : RelativeDateType::NEXT,
110 : : RelativeDateOffset::MONTH,
111 : : "one-month-ahead",
112 : : N_("One Month Ahead"),
113 : : N_("One Month Ahead.")
114 : : },
115 : : {
116 : : RelativeDatePeriod::THREE_MONTHS_AGO,
117 : : RelativeDateType::LAST,
118 : : RelativeDateOffset::THREE,
119 : : "three-months-ago",
120 : : N_("Three Months Ago"),
121 : : N_("Three Months Ago.")
122 : : },
123 : : {
124 : : RelativeDatePeriod::THREE_MONTHS_AHEAD,
125 : : RelativeDateType::NEXT,
126 : : RelativeDateOffset::THREE,
127 : : "three-months-ahead",
128 : : N_("Three Months Ahead"),
129 : : N_("Three Months Ahead.")
130 : : },
131 : : {
132 : : RelativeDatePeriod::SIX_MONTHS_AGO,
133 : : RelativeDateType::LAST,
134 : : RelativeDateOffset::SIX,
135 : : "six-months-ago",
136 : : N_("Six Months Ago"),
137 : : N_("Six Months Ago.")
138 : : },
139 : : {
140 : : RelativeDatePeriod::SIX_MONTHS_AHEAD,
141 : : RelativeDateType::NEXT,
142 : : RelativeDateOffset::SIX,
143 : : "six-months-ahead",
144 : : N_("Six Months Ahead"),
145 : : N_("Six Months Ahead.")
146 : : },
147 : : {
148 : : RelativeDatePeriod::ONE_YEAR_AGO,
149 : : RelativeDateType::LAST,
150 : : RelativeDateOffset::YEAR,
151 : : "one-year-ago",
152 : : N_("One Year Ago"),
153 : : N_("One Year Ago.")
154 : : },
155 : : {
156 : : RelativeDatePeriod::ONE_YEAR_AHEAD,
157 : : RelativeDateType::NEXT,
158 : : RelativeDateOffset::YEAR,
159 : : "one-year-ahead",
160 : : N_("One Year Ahead"),
161 : : N_("One Year Ahead.")
162 : : },
163 : : {
164 : : RelativeDatePeriod::START_THIS_MONTH,
165 : : RelativeDateType::START,
166 : : RelativeDateOffset::MONTH,
167 : : "start-this-month",
168 : : N_("Start of this month"),
169 : : N_("First day of the current month.")
170 : : },
171 : : {
172 : : RelativeDatePeriod::END_THIS_MONTH,
173 : : RelativeDateType::END,
174 : : RelativeDateOffset::MONTH,
175 : : "end-this-month",
176 : : N_("End of this month"),
177 : : N_("Last day of the current month.")
178 : : },
179 : : {
180 : : RelativeDatePeriod::START_PREV_MONTH,
181 : : RelativeDateType::START,
182 : : RelativeDateOffset::MONTH,
183 : : "start-prev-month",
184 : : N_("Start of previous month"),
185 : : N_("First day of the previous month.")
186 : : },
187 : : {
188 : : RelativeDatePeriod::END_PREV_MONTH,
189 : : RelativeDateType::END,
190 : : RelativeDateOffset::MONTH,
191 : : "end-prev-month",
192 : : N_("End of previous month"),
193 : : N_("Last day of previous month.")
194 : : },
195 : : {
196 : : RelativeDatePeriod::START_NEXT_MONTH,
197 : : RelativeDateType::START,
198 : : RelativeDateOffset::MONTH,
199 : : "start-next-month",
200 : : N_("Start of next month"),
201 : : N_("First day of the next month.")
202 : : },
203 : : {
204 : : RelativeDatePeriod::END_NEXT_MONTH,
205 : : RelativeDateType::END,
206 : : RelativeDateOffset::MONTH,
207 : : "end-next-month",
208 : : N_("End of next month"),
209 : : N_("Last day of next month.")
210 : : },
211 : : {
212 : : RelativeDatePeriod::START_CURRENT_QUARTER,
213 : : RelativeDateType::START,
214 : : RelativeDateOffset::QUARTER,
215 : : "start-current-quarter",
216 : : N_("Start of current quarter"),
217 : : N_("First day of the current quarterly accounting period.")
218 : : },
219 : : {
220 : : RelativeDatePeriod::END_CURRENT_QUARTER,
221 : : RelativeDateType::END,
222 : : RelativeDateOffset::QUARTER,
223 : : "end-current-quarter",
224 : : N_("End of current quarter"),
225 : : N_("Last day of the current quarterly accounting period.")
226 : : },
227 : : {
228 : : RelativeDatePeriod::START_PREV_QUARTER,
229 : : RelativeDateType::START,
230 : : RelativeDateOffset::QUARTER,
231 : : "start-prev-quarter",
232 : : N_("Start of previous quarter"),
233 : : N_("First day of the previous quarterly accounting period.")
234 : : },
235 : : {
236 : : RelativeDatePeriod::END_PREV_QUARTER,
237 : : RelativeDateType::END,
238 : : RelativeDateOffset::QUARTER,
239 : : "end-prev-quarter",
240 : : N_("End of previous quarter"),
241 : : N_("Last day of previous quarterly accounting period.")
242 : : },
243 : : {
244 : : RelativeDatePeriod::START_NEXT_QUARTER,
245 : : RelativeDateType::START,
246 : : RelativeDateOffset::QUARTER,
247 : : "start-next-quarter",
248 : : N_("Start of next quarter"),
249 : : N_("First day of the next quarterly accounting period.")
250 : : },
251 : : {
252 : : RelativeDatePeriod::END_NEXT_QUARTER,
253 : : RelativeDateType::END,
254 : : RelativeDateOffset::QUARTER,
255 : : "end-next-quarter",
256 : : N_("End of next quarter"),
257 : : N_("Last day of next quarterly accounting period.")
258 : : },
259 : : {
260 : : RelativeDatePeriod::START_CAL_YEAR,
261 : : RelativeDateType::START,
262 : : RelativeDateOffset::YEAR,
263 : : "start-cal-year",
264 : : N_("Start of this year"),
265 : : N_("First day of the current calendar year.")
266 : : },
267 : : {
268 : : RelativeDatePeriod::END_CAL_YEAR,
269 : : RelativeDateType::END,
270 : : RelativeDateOffset::YEAR,
271 : : "end-cal-year",
272 : : N_("End of this year"),
273 : : N_("Last day of the current calendar year.")
274 : : },
275 : : {
276 : : RelativeDatePeriod::START_PREV_YEAR,
277 : : RelativeDateType::START,
278 : : RelativeDateOffset::YEAR,
279 : : "start-prev-year",
280 : : N_("Start of previous year"),
281 : : N_("First day of the previous calendar year.")
282 : : },
283 : : {
284 : : RelativeDatePeriod::END_PREV_YEAR,
285 : : RelativeDateType::END,
286 : : RelativeDateOffset::YEAR,
287 : : "end-prev-year",
288 : : N_("End of previous year"),
289 : : N_("Last day of the previous calendar year.")
290 : : },
291 : : {
292 : : RelativeDatePeriod::START_NEXT_YEAR,
293 : : RelativeDateType::START,
294 : : RelativeDateOffset::YEAR,
295 : : "start-next-year",
296 : : N_("Start of next year"),
297 : : N_("First day of the next calendar year.")
298 : : },
299 : : {
300 : : RelativeDatePeriod::END_NEXT_YEAR,
301 : : RelativeDateType::END,
302 : : RelativeDateOffset::YEAR,
303 : : "end-next-year",
304 : : N_("End of next year"),
305 : : N_("Last day of the next calendar year.")
306 : : },
307 : : {
308 : : RelativeDatePeriod::START_ACCOUNTING_PERIOD,
309 : : RelativeDateType::START,
310 : : RelativeDateOffset::YEAR,
311 : : "start-prev-fin-year",
312 : : N_("Start of accounting period"),
313 : : N_("First day of the accounting period, as set in the global preferences.")
314 : : },
315 : : {
316 : : RelativeDatePeriod::END_ACCOUNTING_PERIOD,
317 : : RelativeDateType::END,
318 : : RelativeDateOffset::YEAR,
319 : : "end-prev-fin-year",
320 : : N_("End of accounting period"),
321 : : N_("Last day of the accounting period, as set in the global preferences.")
322 : : }
323 : : }};
324 : :
325 : : static const GncRelativeDate&
326 : 943 : checked_reldate(RelativeDatePeriod per)
327 : : {
328 : 943 : assert (reldates[static_cast<int>(per)].m_period == per);
329 : 943 : return reldates[static_cast<int>(per)];
330 : : }
331 : :
332 : : bool
333 : 7 : gnc_relative_date_is_single(RelativeDatePeriod per)
334 : : {
335 : 7 : if (per == RelativeDatePeriod::ABSOLUTE)
336 : 1 : return false;
337 : 6 : auto reldate = checked_reldate(per);
338 : 11 : return reldate.m_type == RelativeDateType::LAST ||
339 : 11 : reldate.m_type == RelativeDateType::NEXT;
340 : : }
341 : :
342 : : bool
343 : 126 : gnc_relative_date_is_starting(RelativeDatePeriod per)
344 : : {
345 : 126 : if (per == RelativeDatePeriod::ABSOLUTE)
346 : 1 : return false;
347 : 125 : return checked_reldate(per).m_type == RelativeDateType::START;
348 : : }
349 : :
350 : : bool
351 : 134 : gnc_relative_date_is_ending(RelativeDatePeriod per)
352 : : {
353 : 134 : if (per == RelativeDatePeriod::ABSOLUTE)
354 : 1 : return false;
355 : 133 : return checked_reldate(per).m_type == RelativeDateType::END;
356 : : }
357 : :
358 : : const char*
359 : 19 : gnc_relative_date_storage_string(RelativeDatePeriod per)
360 : : {
361 : 19 : if (per == RelativeDatePeriod::ABSOLUTE)
362 : 1 : return nullptr;
363 : 18 : return checked_reldate(per).m_storage;
364 : : }
365 : :
366 : : const char*
367 : 2 : gnc_relative_date_display_string(RelativeDatePeriod per)
368 : : {
369 : 2 : if (per == RelativeDatePeriod::ABSOLUTE)
370 : 1 : return nullptr;
371 : 1 : return checked_reldate(per).m_display;
372 : : }
373 : : const char*
374 : 2 : gnc_relative_date_description(RelativeDatePeriod per)
375 : : {
376 : 2 : if (per == RelativeDatePeriod::ABSOLUTE)
377 : 1 : return nullptr;
378 : 1 : return checked_reldate(per).m_description;
379 : : }
380 : :
381 : : RelativeDatePeriod
382 : 15 : gnc_relative_date_from_storage_string(const char* str)
383 : : {
384 : 15 : auto per = std::find_if(reldates.begin(), reldates.end(),
385 : 288 : [str](auto rel) -> bool
386 : : {
387 : 288 : return strcmp(str, rel.m_storage) == 0;
388 : : });
389 : 15 : return per != reldates.end() ? per->m_period : RelativeDatePeriod::ABSOLUTE;
390 : : }
391 : :
392 : : static bool
393 : 177 : reldate_is_prev(RelativeDatePeriod per)
394 : : {
395 : 177 : auto rdate{checked_reldate(per)};
396 : 175 : return per == RelativeDatePeriod::START_PREV_YEAR ||
397 : 153 : per == RelativeDatePeriod::END_PREV_YEAR ||
398 : 141 : per == RelativeDatePeriod::START_PREV_QUARTER ||
399 : 132 : per == RelativeDatePeriod::END_PREV_QUARTER ||
400 : 130 : per == RelativeDatePeriod::START_PREV_MONTH ||
401 : 352 : per == RelativeDatePeriod::END_PREV_MONTH ||
402 : 305 : rdate.m_type == LAST;
403 : : }
404 : :
405 : : static bool
406 : 128 : reldate_is_next(RelativeDatePeriod per)
407 : : {
408 : 128 : auto rdate{checked_reldate(per)};
409 : 128 : return per == RelativeDatePeriod::START_NEXT_YEAR ||
410 : 128 : per == RelativeDatePeriod::END_NEXT_YEAR ||
411 : 124 : per == RelativeDatePeriod::START_NEXT_QUARTER ||
412 : 120 : per == RelativeDatePeriod::END_NEXT_QUARTER ||
413 : 120 : per == RelativeDatePeriod::START_NEXT_MONTH ||
414 : 256 : per == RelativeDatePeriod::END_NEXT_MONTH ||
415 : 248 : rdate.m_type == NEXT;
416 : : }
417 : :
418 : : static RelativeDateOffset
419 : 177 : reldate_offset(RelativeDatePeriod per)
420 : : {
421 : 177 : return checked_reldate(per).m_offset;
422 : : }
423 : :
424 : : static int
425 : 317 : days_in_month(int month, int year)
426 : : {
427 : 317 : return gnc_date_get_last_mday(month, year + 1900);
428 : : }
429 : :
430 : : static int
431 : 140 : get_last_day_of_month(struct tm& now)
432 : : {
433 : : /* Ensure that the month is between 0 and 11*/
434 : 140 : auto year_delta = (now.tm_mon / 12) + (now.tm_mon < 0 ? -1 : 0);
435 : 140 : return days_in_month(now.tm_mon - (12 * year_delta), now.tm_year + year_delta);
436 : : }
437 : :
438 : : static void
439 : 107 : set_last_day_in_month(struct tm& now)
440 : : {
441 : 107 : now.tm_mday = get_last_day_of_month(now);
442 : 107 : }
443 : :
444 : : static bool
445 : 33 : is_last_day_in_month(struct tm& now)
446 : : {
447 : 33 : return now.tm_mday == get_last_day_of_month(now);
448 : : }
449 : :
450 : : /* Normalize the modified struct tm computed in gnc_relative_date_to_time64
451 : : * before setting the time and perhaps beginning/end of the month. Using the
452 : : * gnc_date API would involve multiple conversions to and from struct tm.
453 : : */
454 : : static void
455 : 177 : normalize_reldate_tm(struct tm& now)
456 : : {
457 : 177 : auto delta = (now.tm_mon / 12) + (now.tm_mon < 0 ? -1 : 0);
458 : 177 : now.tm_mon -= 12 * delta;
459 : 177 : now.tm_year += delta;
460 : :
461 : 177 : if (now.tm_mday < 1)
462 : : {
463 : : do
464 : : {
465 : 0 : if (now.tm_mon-- == 0)
466 : : {
467 : 0 : now.tm_mon = 11;
468 : 0 : now.tm_year--;
469 : : }
470 : 0 : now.tm_mday += days_in_month(now.tm_mon, now.tm_year);
471 : 0 : } while (now.tm_mday < 1) ;
472 : 0 : return;
473 : : }
474 : :
475 : 177 : while (now.tm_mday > (delta = days_in_month(now.tm_mon, now.tm_year)))
476 : : {
477 : 0 : if (now.tm_mon++ == 11)
478 : : {
479 : 0 : now.tm_mon = 0;
480 : 0 : now.tm_year++;
481 : : }
482 : 0 : now.tm_mday -= delta;
483 : : }
484 : : }
485 : :
486 : : static void
487 : 177 : reldate_set_day_and_time(
488 : : struct tm& now,
489 : : RelativeDateType type,
490 : : bool is_offset_quarter,
491 : : struct tm& acct_per)
492 : : {
493 : 177 : if (type == RelativeDateType::START)
494 : : {
495 : 79 : now.tm_mday = 1;
496 : 79 : if (is_offset_quarter)
497 : : {
498 : 9 : set_last_day_in_month(now);
499 : 9 : if (!is_last_day_in_month(acct_per) && now.tm_mday > acct_per.tm_mday)
500 : 9 : now.tm_mday = acct_per.tm_mday;
501 : : }
502 : 79 : gnc_tm_set_day_start(&now);
503 : : }
504 : 98 : else if (type == RelativeDateType::END)
505 : : {
506 : 98 : set_last_day_in_month(now);
507 : 98 : if (is_offset_quarter)
508 : : {
509 : 6 : if (!is_last_day_in_month(acct_per) && now.tm_mday > acct_per.tm_mday)
510 : 6 : now.tm_mday = acct_per.tm_mday;
511 : 6 : --now.tm_mday;
512 : : }
513 : 98 : gnc_tm_set_day_end(&now);
514 : : }
515 : : // Do nothing for LAST and NEXT.
516 : 177 : };
517 : :
518 : : time64
519 : 614 : gnc_relative_date_to_time64(RelativeDatePeriod period, time64 now_t)
520 : : {
521 : 614 : if (period == RelativeDatePeriod::TODAY)
522 : 51 : return now_t;
523 : 563 : if (period == RelativeDatePeriod::START_ACCOUNTING_PERIOD)
524 : 175 : return gnc_accounting_period_fiscal_start();
525 : 388 : if (period == RelativeDatePeriod::END_ACCOUNTING_PERIOD)
526 : 211 : return gnc_accounting_period_fiscal_end();
527 : :
528 : 177 : auto now{static_cast<tm>(GncDateTime(now_t))};
529 : : struct tm acct_per =
530 : 177 : static_cast<tm>(GncDateTime(gnc_accounting_period_fiscal_start()));
531 : 177 : auto offset = reldate_offset(period);
532 : 177 : bool is_offset_quarter =
533 : 177 : offset == RelativeDateOffset::QUARTER && acct_per.tm_mday > 1;
534 : :
535 : 177 : switch(offset)
536 : : {
537 : 0 : case RelativeDateOffset::NONE:
538 : : // Report on today so nothing to do
539 : 0 : break;
540 : 119 : case RelativeDateOffset::YEAR:
541 : 119 : if (reldate_is_prev(period))
542 : 24 : --now.tm_year;
543 : 95 : else if (reldate_is_next(period))
544 : 0 : ++now.tm_year;
545 : 119 : if (gnc_relative_date_is_starting(period))
546 : 48 : now.tm_mon = 0;
547 : 71 : else if (gnc_relative_date_is_ending(period))
548 : 71 : now.tm_mon = 11;
549 : 119 : break;
550 : 0 : case RelativeDateOffset::SIX:
551 : 0 : if (reldate_is_prev(period))
552 : 0 : now.tm_mon -= 6;
553 : 0 : else if (reldate_is_next(period))
554 : 0 : now.tm_mon += 6;
555 : 0 : break;
556 : 41 : case RelativeDateOffset::QUARTER:
557 : : {
558 : 41 : auto delta = (12 + now.tm_mon - acct_per.tm_mon) % 3;
559 : 41 : if (is_offset_quarter)
560 : : {
561 : 24 : if (delta == 0 && !is_last_day_in_month(now) &&
562 : 9 : (is_last_day_in_month(acct_per) ||
563 : 9 : now.tm_mday < acct_per.tm_mday))
564 : 0 : delta = 3;
565 : 15 : if (gnc_relative_date_is_ending(period))
566 : 6 : --delta;
567 : : }
568 : 41 : now.tm_mon -= delta;
569 : : }
570 : : [[fallthrough]];
571 : 41 : case RelativeDateOffset::THREE:
572 : 41 : if (reldate_is_prev(period))
573 : 21 : now.tm_mon -= 3;
574 : 20 : else if (reldate_is_next(period))
575 : 8 : now.tm_mon += 3;
576 : 41 : if (gnc_relative_date_is_ending(period))
577 : 19 : now.tm_mon += 2;
578 : 41 : break;
579 : 17 : case RelativeDateOffset::MONTH:
580 : 17 : if (reldate_is_prev(period))
581 : 4 : --now.tm_mon;
582 : 13 : else if (reldate_is_next(period))
583 : 0 : ++now.tm_mon;
584 : 17 : break;
585 : 0 : case RelativeDateOffset::WEEK:
586 : 0 : if (reldate_is_prev(period))
587 : 0 : now.tm_mday -= 7;
588 : 0 : else if (reldate_is_next(period))
589 : 0 : now.tm_mday += 7;
590 : : }
591 : 354 : reldate_set_day_and_time( now,
592 : 177 : checked_reldate(period).m_type,
593 : : is_offset_quarter,
594 : : acct_per);
595 : 177 : normalize_reldate_tm(now);
596 : 177 : return static_cast<time64>(GncDateTime(now));
597 : : }
598 : :
599 : : std::ostream&
600 : 0 : operator<<(std::ostream& ostr, RelativeDatePeriod per)
601 : : {
602 : 0 : ostr << "'reldate . " << gnc_relative_date_display_string(per);
603 : 0 : return ostr;
604 : : }
|