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 813 : checked_reldate(RelativeDatePeriod per)
327 : {
328 813 : assert (reldates[static_cast<int>(per)].m_period == per);
329 813 : 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 106 : gnc_relative_date_is_starting(RelativeDatePeriod per)
344 : {
345 106 : if (per == RelativeDatePeriod::ABSOLUTE)
346 1 : return false;
347 105 : return checked_reldate(per).m_type == RelativeDateType::START;
348 : }
349 :
350 : bool
351 102 : gnc_relative_date_is_ending(RelativeDatePeriod per)
352 : {
353 102 : if (per == RelativeDatePeriod::ABSOLUTE)
354 1 : return false;
355 101 : 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 151 : reldate_is_prev(RelativeDatePeriod per)
394 : {
395 151 : auto rdate{checked_reldate(per)};
396 149 : return per == RelativeDatePeriod::START_PREV_YEAR ||
397 147 : per == RelativeDatePeriod::END_PREV_YEAR ||
398 139 : per == RelativeDatePeriod::START_PREV_QUARTER ||
399 132 : per == RelativeDatePeriod::END_PREV_QUARTER ||
400 130 : per == RelativeDatePeriod::START_PREV_MONTH ||
401 300 : per == RelativeDatePeriod::END_PREV_MONTH ||
402 279 : 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 151 : reldate_offset(RelativeDatePeriod per)
420 : {
421 151 : return checked_reldate(per).m_offset;
422 : }
423 :
424 : static int
425 247 : days_in_month(int month, int year)
426 : {
427 247 : return gnc_date_get_last_mday(month, year + 1900);
428 : }
429 :
430 : static int
431 96 : get_last_day_of_month(struct tm& now)
432 : {
433 : /* Ensure that the month is between 0 and 11*/
434 96 : auto year_delta = (now.tm_mon / 12) + (now.tm_mon < 0 ? -1 : 0);
435 96 : return days_in_month(now.tm_mon - (12 * year_delta), now.tm_year + year_delta);
436 : }
437 :
438 : static void
439 81 : set_last_day_in_month(struct tm& now)
440 : {
441 81 : now.tm_mday = get_last_day_of_month(now);
442 81 : }
443 :
444 : static bool
445 15 : is_last_day_in_month(struct tm& now)
446 : {
447 15 : 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 151 : normalize_reldate_tm(struct tm& now)
456 : {
457 151 : auto delta = (now.tm_mon / 12) + (now.tm_mon < 0 ? -1 : 0);
458 151 : now.tm_mon -= 12 * delta;
459 151 : now.tm_year += delta;
460 :
461 151 : 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 151 : 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 151 : reldate_set_day_and_time(
488 : struct tm& now,
489 : RelativeDateType type,
490 : bool is_offset_quarter,
491 : struct tm& acct_per)
492 : {
493 151 : if (type == RelativeDateType::START)
494 : {
495 75 : now.tm_mday = 1;
496 75 : if (is_offset_quarter)
497 : {
498 5 : set_last_day_in_month(now);
499 5 : if (!is_last_day_in_month(acct_per) && now.tm_mday > acct_per.tm_mday)
500 5 : now.tm_mday = acct_per.tm_mday;
501 : }
502 75 : gnc_tm_set_day_start(&now);
503 : }
504 76 : else if (type == RelativeDateType::END)
505 : {
506 76 : set_last_day_in_month(now);
507 76 : if (is_offset_quarter)
508 : {
509 4 : if (!is_last_day_in_month(acct_per) && now.tm_mday > acct_per.tm_mday)
510 4 : now.tm_mday = acct_per.tm_mday;
511 4 : --now.tm_mday;
512 : }
513 76 : gnc_tm_set_day_end(&now);
514 : }
515 : // Do nothing for LAST and NEXT.
516 151 : };
517 :
518 : time64
519 328 : gnc_relative_date_to_time64(RelativeDatePeriod period, time64 now_t)
520 : {
521 328 : if (period == RelativeDatePeriod::TODAY)
522 65 : return now_t;
523 263 : if (period == RelativeDatePeriod::START_ACCOUNTING_PERIOD)
524 73 : return gnc_accounting_period_fiscal_start();
525 190 : if (period == RelativeDatePeriod::END_ACCOUNTING_PERIOD)
526 39 : return gnc_accounting_period_fiscal_end();
527 :
528 151 : auto now{static_cast<tm>(GncDateTime(now_t))};
529 : struct tm acct_per =
530 151 : static_cast<tm>(GncDateTime(gnc_accounting_period_fiscal_start()));
531 151 : auto offset = reldate_offset(period);
532 151 : bool is_offset_quarter =
533 151 : offset == RelativeDateOffset::QUARTER && acct_per.tm_mday > 1;
534 :
535 151 : switch(offset)
536 : {
537 0 : case RelativeDateOffset::NONE:
538 : // Report on today so nothing to do
539 0 : break;
540 99 : case RelativeDateOffset::YEAR:
541 99 : if (reldate_is_prev(period))
542 4 : --now.tm_year;
543 95 : else if (reldate_is_next(period))
544 0 : ++now.tm_year;
545 99 : if (gnc_relative_date_is_starting(period))
546 48 : now.tm_mon = 0;
547 51 : else if (gnc_relative_date_is_ending(period))
548 51 : now.tm_mon = 11;
549 99 : 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 35 : case RelativeDateOffset::QUARTER:
557 : {
558 35 : auto delta = (12 + now.tm_mon - acct_per.tm_mon) % 3;
559 35 : if (is_offset_quarter)
560 : {
561 12 : if (delta == 0 && !is_last_day_in_month(now) &&
562 3 : (is_last_day_in_month(acct_per) ||
563 3 : now.tm_mday < acct_per.tm_mday))
564 0 : delta = 3;
565 9 : if (gnc_relative_date_is_ending(period))
566 4 : --delta;
567 : }
568 35 : now.tm_mon -= delta;
569 : }
570 : [[fallthrough]];
571 35 : case RelativeDateOffset::THREE:
572 35 : if (reldate_is_prev(period))
573 15 : now.tm_mon -= 3;
574 20 : else if (reldate_is_next(period))
575 8 : now.tm_mon += 3;
576 35 : if (gnc_relative_date_is_ending(period))
577 17 : now.tm_mon += 2;
578 35 : 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 302 : reldate_set_day_and_time( now,
592 151 : checked_reldate(period).m_type,
593 : is_offset_quarter,
594 : acct_per);
595 151 : normalize_reldate_tm(now);
596 151 : 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 : }
|