GnuCash  4.11-517-g41de4cefce
gnc-option-date.cpp
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 <iostream>
27 #include <cassert>
28 #include <algorithm>
29 
30 extern "C"
31 {
32 #include <gnc-accounting-period.h>
33 }
34 
35 #define N_(string) string //So that xgettext will find it
36 
37 enum RelativeDateType
38 {
39  ABSOLUTE,
40  LAST,
41  NEXT,
42  START,
43  END
44 };
45 
46 enum RelativeDateOffset
47 {
48  NONE,
49  WEEK,
50  MONTH,
51  QUARTER,
52  THREE,
53  SIX,
54  YEAR
55 };
56 
58 {
59  RelativeDatePeriod m_period;
60  RelativeDateType m_type;
61  RelativeDateOffset m_offset;
62  const char* m_storage;
63  const char* m_display;
64  const char* m_description;
65 };
66 
67 
68 /* The fixed values and strings for date periods. Accessor functions will use
69  * the RelativeDatePeriod as an index so any changes need to be reflected in the
70  * RelativeDatePeriod enum class in gnc-option-date.hpp and vice-versa.
71  *
72  * The double curly braces are actually correct and required for a std::array
73  * initializer list.
74  */
75 static const std::array<GncRelativeDate, 31> reldates
76 {{
77  {
78  RelativeDatePeriod::TODAY,
79  RelativeDateType::LAST,
80  RelativeDateOffset::NONE,
81  "today",
82  N_("Today"),
83  N_("The current date.")
84  },
85  {
86  RelativeDatePeriod::ONE_WEEK_AGO,
87  RelativeDateType::LAST,
88  RelativeDateOffset::WEEK,
89  "one-week-ago",
90  N_("One Week Ago"),
91  N_("One Week Ago.")
92  },
93  {
94  RelativeDatePeriod::ONE_WEEK_AHEAD,
95  RelativeDateType::NEXT,
96  RelativeDateOffset::WEEK,
97  "one-week-ahead",
98  N_("One Week Ahead"),
99  N_("One Week Ahead.")
100  },
101  {
102  RelativeDatePeriod::ONE_MONTH_AGO,
103  RelativeDateType::LAST,
104  RelativeDateOffset::MONTH,
105  "one-month-ago",
106  N_("One Month Ago"),
107  N_("One Month Ago.")
108  },
109  {
110  RelativeDatePeriod::ONE_MONTH_AHEAD,
111  RelativeDateType::NEXT,
112  RelativeDateOffset::MONTH,
113  "one-month-ahead",
114  N_("One Month Ahead"),
115  N_("One Month Ahead.")
116  },
117  {
118  RelativeDatePeriod::THREE_MONTHS_AGO,
119  RelativeDateType::LAST,
120  RelativeDateOffset::THREE,
121  "three-months-ago",
122  N_("Three Months Ago"),
123  N_("Three Months Ago.")
124  },
125  {
126  RelativeDatePeriod::THREE_MONTHS_AHEAD,
127  RelativeDateType::NEXT,
128  RelativeDateOffset::THREE,
129  "three-months-ahead",
130  N_("Three Months Ahead"),
131  N_("Three Months Ahead.")
132  },
133  {
134  RelativeDatePeriod::SIX_MONTHS_AGO,
135  RelativeDateType::LAST,
136  RelativeDateOffset::SIX,
137  "six-months-ago",
138  N_("Six Months Ago"),
139  N_("Six Months Ago.")
140  },
141  {
142  RelativeDatePeriod::SIX_MONTHS_AHEAD,
143  RelativeDateType::NEXT,
144  RelativeDateOffset::SIX,
145  "six-months-ahead",
146  N_("Six Months Ahead"),
147  N_("Six Months Ahead.")
148  },
149  {
150  RelativeDatePeriod::ONE_YEAR_AGO,
151  RelativeDateType::LAST,
152  RelativeDateOffset::YEAR,
153  "one-year-ago",
154  N_("One Year Ago"),
155  N_("One Year Ago.")
156  },
157  {
158  RelativeDatePeriod::ONE_YEAR_AHEAD,
159  RelativeDateType::NEXT,
160  RelativeDateOffset::YEAR,
161  "one-year-ahead",
162  N_("One Year Ahead"),
163  N_("One Year Ahead.")
164  },
165  {
166  RelativeDatePeriod::START_THIS_MONTH,
167  RelativeDateType::START,
168  RelativeDateOffset::MONTH,
169  "start-this-month",
170  N_("Start of this month"),
171  N_("First day of the current month.")
172  },
173  {
174  RelativeDatePeriod::END_THIS_MONTH,
175  RelativeDateType::END,
176  RelativeDateOffset::MONTH,
177  "end-this-month",
178  N_("End of this month"),
179  N_("Last day of the current month.")
180  },
181  {
182  RelativeDatePeriod::START_PREV_MONTH,
183  RelativeDateType::START,
184  RelativeDateOffset::MONTH,
185  "start-prev-month",
186  N_("Start of previous month"),
187  N_("First day of the previous month.")
188  },
189  {
190  RelativeDatePeriod::END_PREV_MONTH,
191  RelativeDateType::END,
192  RelativeDateOffset::MONTH,
193  "end-prev-month",
194  N_("End of previous month"),
195  N_("Last day of previous month.")
196  },
197  {
198  RelativeDatePeriod::START_NEXT_MONTH,
199  RelativeDateType::START,
200  RelativeDateOffset::MONTH,
201  "start-next-month",
202  N_("Start of next month"),
203  N_("First day of the next month.")
204  },
205  {
206  RelativeDatePeriod::END_NEXT_MONTH,
207  RelativeDateType::END,
208  RelativeDateOffset::MONTH,
209  "end-next-month",
210  N_("End of next month"),
211  N_("Last day of next month.")
212  },
213  {
214  RelativeDatePeriod::START_CURRENT_QUARTER,
215  RelativeDateType::START,
216  RelativeDateOffset::QUARTER,
217  "start-current-quarter",
218  N_("Start of current quarter"),
219  N_("First day of the current quarterly accounting period.")
220  },
221  {
222  RelativeDatePeriod::END_CURRENT_QUARTER,
223  RelativeDateType::END,
224  RelativeDateOffset::QUARTER,
225  "end-current-quarter",
226  N_("End of current quarter"),
227  N_("Last day of the current quarterly accounting period.")
228  },
229  {
230  RelativeDatePeriod::START_PREV_QUARTER,
231  RelativeDateType::START,
232  RelativeDateOffset::QUARTER,
233  "start-prev-quarter",
234  N_("Start of previous quarter"),
235  N_("First day of the previous quarterly accounting period.")
236  },
237  {
238  RelativeDatePeriod::END_PREV_QUARTER,
239  RelativeDateType::END,
240  RelativeDateOffset::QUARTER,
241  "end-prev-quarter",
242  N_("End of previous quarter"),
243  N_("Last day of previous quarterly accounting period.")
244  },
245  {
246  RelativeDatePeriod::START_NEXT_QUARTER,
247  RelativeDateType::START,
248  RelativeDateOffset::QUARTER,
249  "start-next-quarter",
250  N_("Start of next quarter"),
251  N_("First day of the next quarterly accounting period.")
252  },
253  {
254  RelativeDatePeriod::END_NEXT_QUARTER,
255  RelativeDateType::END,
256  RelativeDateOffset::QUARTER,
257  "end-next-quarter",
258  N_("End of next quarter"),
259  N_("Last day of next quarterly accounting period.")
260  },
261  {
262  RelativeDatePeriod::START_CAL_YEAR,
263  RelativeDateType::START,
264  RelativeDateOffset::YEAR,
265  "start-cal-year",
266  N_("Start of this year"),
267  N_("First day of the current calendar year.")
268  },
269  {
270  RelativeDatePeriod::END_CAL_YEAR,
271  RelativeDateType::END,
272  RelativeDateOffset::YEAR,
273  "end-cal-year",
274  N_("End of this year"),
275  N_("Last day of the current calendar year.")
276  },
277  {
278  RelativeDatePeriod::START_PREV_YEAR,
279  RelativeDateType::START,
280  RelativeDateOffset::YEAR,
281  "start-prev-year",
282  N_("Start of previous year"),
283  N_("First day of the previous calendar year.")
284  },
285  {
286  RelativeDatePeriod::END_PREV_YEAR,
287  RelativeDateType::END,
288  RelativeDateOffset::YEAR,
289  "end-prev-year",
290  N_("End of previous year"),
291  N_("Last day of the previous calendar year.")
292  },
293  {
294  RelativeDatePeriod::START_NEXT_YEAR,
295  RelativeDateType::START,
296  RelativeDateOffset::YEAR,
297  "start-next-year",
298  N_("Start of next year"),
299  N_("First day of the next calendar year.")
300  },
301  {
302  RelativeDatePeriod::END_NEXT_YEAR,
303  RelativeDateType::END,
304  RelativeDateOffset::YEAR,
305  "end-next-year",
306  N_("End of next year"),
307  N_("Last day of the next calendar year.")
308  },
309  {
310  RelativeDatePeriod::START_ACCOUNTING_PERIOD,
311  RelativeDateType::START,
312  RelativeDateOffset::YEAR,
313  "start-prev-fin-year",
314  N_("Start of accounting period"),
315  N_("First day of the accounting period, as set in the global preferences.")
316  },
317  {
318  RelativeDatePeriod::END_ACCOUNTING_PERIOD,
319  RelativeDateType::END,
320  RelativeDateOffset::YEAR,
321  "end-prev-fin-year",
322  N_("End of accounting period"),
323  N_("Last day of the accounting period, as set in the global preferences.")
324  }
325  }};
326 
327 static const GncRelativeDate&
328 checked_reldate(RelativeDatePeriod per)
329 {
330  assert (reldates[static_cast<int>(per)].m_period == per);
331  return reldates[static_cast<int>(per)];
332 }
333 
334 bool
336 {
337  if (per == RelativeDatePeriod::ABSOLUTE)
338  return false;
339  auto reldate = checked_reldate(per);
340  return reldate.m_type == RelativeDateType::LAST ||
341  reldate.m_type == RelativeDateType::NEXT;
342 }
343 
344 bool
346 {
347  if (per == RelativeDatePeriod::ABSOLUTE)
348  return false;
349  return checked_reldate(per).m_type == RelativeDateType::START;
350 }
351 
352 bool
354 {
355  if (per == RelativeDatePeriod::ABSOLUTE)
356  return false;
357  return checked_reldate(per).m_type == RelativeDateType::END;
358 }
359 
360 const char*
362 {
363  if (per == RelativeDatePeriod::ABSOLUTE)
364  return nullptr;
365  return checked_reldate(per).m_storage;
366 }
367 
368 const char*
370 {
371  if (per == RelativeDatePeriod::ABSOLUTE)
372  return nullptr;
373  return checked_reldate(per).m_display;
374 }
375 const char*
377 {
378  if (per == RelativeDatePeriod::ABSOLUTE)
379  return nullptr;
380  return checked_reldate(per).m_description;
381 }
382 
385 {
386  auto per = std::find_if(reldates.begin(), reldates.end(),
387  [str](auto rel) -> bool
388  {
389  return strcmp(str, rel.m_storage) == 0;
390  });
391  return per != reldates.end() ? per->m_period : RelativeDatePeriod::ABSOLUTE;
392 }
393 
394 static bool
395 reldate_is_prev(RelativeDatePeriod per)
396 {
397  auto rdate{checked_reldate(per)};
398  return per == RelativeDatePeriod::START_PREV_YEAR ||
399  per == RelativeDatePeriod::END_PREV_YEAR ||
400  per == RelativeDatePeriod::START_PREV_QUARTER ||
401  per == RelativeDatePeriod::END_PREV_QUARTER ||
402  per == RelativeDatePeriod::START_PREV_MONTH ||
403  per == RelativeDatePeriod::END_PREV_MONTH ||
404  rdate.m_type == LAST;
405 }
406 
407 static bool
408 reldate_is_next(RelativeDatePeriod per)
409 {
410  auto rdate{checked_reldate(per)};
411  return per == RelativeDatePeriod::START_NEXT_YEAR ||
412  per == RelativeDatePeriod::END_NEXT_YEAR ||
413  per == RelativeDatePeriod::START_NEXT_QUARTER ||
414  per == RelativeDatePeriod::END_NEXT_QUARTER ||
415  per == RelativeDatePeriod::START_NEXT_MONTH ||
416  per == RelativeDatePeriod::END_NEXT_MONTH ||
417  rdate.m_type == NEXT;
418 }
419 
420 static RelativeDateOffset
421 reldate_offset(RelativeDatePeriod per)
422 {
423  return checked_reldate(per).m_offset;
424 }
425 
426 static constexpr int days_in_month[12]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
427 
428 /* Normalize the modified struct tm computed in gnc_relative_date_to_time64
429  * before setting the time and perhaps beginning/end of the month. Using the
430  * gnc_date API would involve multiple conversions to and from struct tm.
431 */
432 static void
433 normalize_reldate_tm(struct tm& now)
434 {
435  auto factor{abs(now.tm_mon) / 12};
436  now.tm_mon /= factor > 0 ? factor : 1;
437  now.tm_year += now.tm_mon < 0 ? -factor: factor;
438 
439  auto days = [](auto month, int year)
440  {
441  auto mon{month % 12 + (month < 0 ? 12 : 0)};
442  auto num_days{days_in_month[mon]};
443  //Leap year check.
444  if (mon == 1 && year % 4 == 0 && !(year % 100 == 0 && (year + 1900) % 400 != 0))
445  ++num_days;
446  return num_days;
447  };
448 
449  while (now.tm_mday < 1)
450  now.tm_mday += days(--now.tm_mon, now.tm_year);
451 
452  while (now.tm_mday > days(now.tm_mon, now.tm_year))
453  now.tm_mday -= days(now.tm_mon++, now.tm_year);
454 
455  while (now.tm_mon < 0)
456  {
457  now.tm_mon += 12;
458  --now.tm_year;
459  }
460  while (now.tm_mon > 11)
461  {
462  now.tm_mon -= 12;
463  ++now.tm_year;
464  }
465 
466  /* This would happen only if we moved from Feb 29 in a leap year while
467  * adjusting the months so we don't need to worry about adjusting the year
468  * again.
469  */
470  if (now.tm_mday > days_in_month[now.tm_mon])
471  now.tm_mday -= days_in_month[now.tm_mon++];
472 }
473 
474 static void
475 reldate_set_day_and_time(struct tm& now, RelativeDateType type)
476 {
477  if (type == RelativeDateType::START)
478  {
479  gnc_tm_set_day_start(&now);
480  now.tm_mday = 1;
481  }
482  else if (type == RelativeDateType::END)
483  {
484  /* Ensure that the month is between 0 and 12*/
485  auto year_delta = now.tm_mon / 12 + now.tm_mon < 0 ? -1 : 0;
486  auto month = now.tm_mon - 12 * year_delta;
487  auto year = now.tm_year + year_delta + 1900;
488  now.tm_mday = gnc_date_get_last_mday(month, year);
489  gnc_tm_set_day_end(&now);
490  }
491  // Do nothing for LAST and NEXT.
492 };
493 
494 time64
496 {
497  if (period == RelativeDatePeriod::TODAY)
498  return static_cast<time64>(GncDateTime());
499  if (period == RelativeDatePeriod::START_ACCOUNTING_PERIOD)
500  return gnc_accounting_period_fiscal_start();
501  if (period == RelativeDatePeriod::END_ACCOUNTING_PERIOD)
502  return gnc_accounting_period_fiscal_end();
503 
504  GncDateTime now_t;
505  if (period == RelativeDatePeriod::TODAY)
506  return static_cast<time64>(now_t);
507  auto now{static_cast<tm>(now_t)};
508  auto acct_per{static_cast<tm>(GncDateTime(gnc_accounting_period_fiscal_start()))};
509 
510  if (acct_per.tm_mon == now.tm_mon && acct_per.tm_mday == now.tm_mday)
511  {
512  //No set accounting period, use the calendar year
513  acct_per.tm_mon = 0;
514  acct_per.tm_mday = 0;
515  }
516 
517  switch(reldate_offset(period))
518  {
519  case RelativeDateOffset::NONE:
520 // Report on today so nothing to do
521  break;
522  case RelativeDateOffset::YEAR:
523  if (reldate_is_prev(period))
524  --now.tm_year;
525  else if (reldate_is_next(period))
526  ++now.tm_year;
527  if (gnc_relative_date_is_starting(period))
528  now.tm_mon = 0;
529  else if (gnc_relative_date_is_ending(period))
530  now.tm_mon = 11;
531  break;
532  case RelativeDateOffset::SIX:
533  if (reldate_is_prev(period))
534  now.tm_mon -= 6;
535  else if (reldate_is_next(period))
536  now.tm_mon += 6;
537  break;
538  case RelativeDateOffset::QUARTER:
539  {
540  auto delta = (now.tm_mon > acct_per.tm_mon ?
541  now.tm_mon - acct_per.tm_mon :
542  acct_per.tm_mon - now.tm_mon) % 3;
543  now.tm_mon = now.tm_mon - delta;
544  }
545  [[fallthrough]];
546  case RelativeDateOffset::THREE:
547  if (reldate_is_prev(period))
548  now.tm_mon -= 3;
549  else if (reldate_is_next(period))
550  now.tm_mon += 3;
551  if (gnc_relative_date_is_ending(period))
552  now.tm_mon += 2;
553  break;
554  case RelativeDateOffset::MONTH:
555  if (reldate_is_prev(period))
556  --now.tm_mon;
557  else if (reldate_is_next(period))
558  ++now.tm_mon;
559  break;
560  case RelativeDateOffset::WEEK:
561  if (reldate_is_prev(period))
562  now.tm_mday -= 7;
563  else if (reldate_is_next(period))
564  now.tm_mday += 7;
565  }
566  reldate_set_day_and_time(now, checked_reldate(period).m_type);
567  normalize_reldate_tm(now);
568  return static_cast<time64>(GncDateTime(now));
569 }
570 
571 std::ostream&
572 operator<<(std::ostream& ostr, RelativeDatePeriod per)
573 {
574  ostr << "'reldate . " << gnc_relative_date_display_string(per);
575  return ostr;
576 }
std::ostream & operator<<(std::ostream &ostr, RelativeDatePeriod per)
Add the display string to the provided std::ostream.
GnuCash DateTime class.
RelativeDatePeriod gnc_relative_date_from_storage_string(const char *str)
Convert a relative date storage string back to a RelativeDatePeriod value.
const char * gnc_relative_date_display_string(RelativeDatePeriod per)
Provide the string representation of a relative date for displaying value to a user.
const char * gnc_relative_date_description(RelativeDatePeriod per)
Provide the description of a relative date.
int gnc_date_get_last_mday(int month, int year)
Get the numerical last date of the month.
Definition: gnc-date.cpp:422
time64 gnc_relative_date_to_time64(RelativeDatePeriod period)
Convert a RelativeDatePeriod value to a concrete time64 by applying the value to the current time...
bool gnc_relative_date_is_ending(RelativeDatePeriod per)
Report whether the relative date represents the end of a date range.
bool gnc_relative_date_is_single(RelativeDatePeriod per)
Report whether the relative date represents a period offset to today&#39;s date rather than the beginning...
RelativeDatePeriod
Reporting periods relative to the current date.
General utilities for dealing with accounting periods.
bool gnc_relative_date_is_starting(RelativeDatePeriod per)
Report whether the relative date represents the beginning of a date range.
const char * gnc_relative_date_storage_string(RelativeDatePeriod per)
Provide the string representation of a relative date for persisting the value.
gint64 time64
Many systems, including Microsoft Windows and BSD-derived Unixes like Darwin, are retaining the int-3...
Definition: gnc-date.h:93
Relative date enumeration and manipulation functions.