Engauge Digitizer  2
FormatDateTime.cpp
Go to the documentation of this file.
1 /******************************************************************************************************
2  * (C) 2014 markummitchell@github.com. This file is part of Engauge Digitizer, which is released *
3  * under GNU General Public License version 2 (GPLv2) or (at your option) any later version. See file *
4  * LICENSE or go to gnu.org/licenses for details. Distribution requires prior written permission. *
5  ******************************************************************************************************/
6 
7 #include "EngaugeAssert.h"
8 #include "FormatDateTime.h"
9 #include "Logger.h"
10 #include <QDateTime>
11 #include <qmath.h>
12 #include <QTimeZone>
13 
14 // Need a reference time zone so exported outputs do not exhibit unpredictable local/UTC hours differences
15 const Qt::TimeSpec REFERENCE_TIME_ZONE (Qt::UTC);
16 
18 {
19  loadFormatsFormat();
20  loadFormatsParseAcceptable();
21  loadFormatsParseIncomplete();
22 }
23 
24 bool FormatDateTime::ambiguityBetweenDateAndTime (CoordUnitsDate coordUnitsDate,
25  CoordUnitsTime coordUnitsTime,
26  const QString &string) const
27 {
28  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::ambiguityBetweenDateAndTime";
29 
30  bool ambiguous = false;
31 
32  // There is no ambiguity if user specified either date or time as empty
33  if (coordUnitsDate != COORD_UNITS_DATE_SKIP &&
34  coordUnitsTime != COORD_UNITS_TIME_SKIP) {
35 
36  // See if there is just a single number
37  QStringList fields = string.trimmed().split(QRegExp ("[/- :]"));
38 
39  if (fields.count() == 1) {
40 
41  // There is a single number. Since there are no attached delimiters to differentiate a date versus
42  // a time, this means the number is ambiguous
43  ambiguous = true;
44  }
45  }
46 
47  return ambiguous;
48 }
49 
50 void FormatDateTime::dateTimeLookup (const FormatsDate &formatsDateAll,
51  const FormatsTime &formatsTimeAll,
52  CoordUnitsDate coordUnitsDate,
53  CoordUnitsTime coordUnitsTime,
54  const QString &string,
55  bool useQDateTimeElseQRegExp,
56  double &value,
57  bool &success) const
58 {
59  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::dateTimeLookup";
60 
61  success = false;
62 
63  ENGAUGE_ASSERT (formatsDateAll.contains (coordUnitsDate));
64  ENGAUGE_ASSERT (formatsTimeAll.contains (coordUnitsTime));
65 
66  QStringList formatsDate = formatsDateAll [coordUnitsDate];
67  QStringList formatsTime = formatsTimeAll [coordUnitsTime];
68 
69  // Loop through the legal date/time combinations
70  QStringList::const_iterator itrDate, itrTime;
71  bool iterating = true;
72  for (itrDate = formatsDate.begin(); itrDate != formatsDate.end() && iterating; itrDate++) {
73 
74  QString formatDate = *itrDate;
75 
76  for (itrTime = formatsTime.begin(); itrTime != formatsTime.end() && iterating; itrTime++) {
77 
78  QString formatTime = *itrTime;
79 
80  // Insert space as separator only if needed. Do not use trim around here since formatDate may or may not end in a space
81  QString separator = (!formatDate.isEmpty() && !formatTime.isEmpty() ? " " : "");
82 
83  QString formatDateTime = formatDate + separator + formatTime;
84 
85  if (!formatDateTime.isEmpty()) {
86 
87  // Try parsing according to the current format
88  if (useQDateTimeElseQRegExp) {
89 
90  QDateTime dt = QDateTime::fromString (string,
91  formatDateTime);
92 
93  if (dt.isValid() && !ambiguityBetweenDateAndTime (coordUnitsDate,
94  coordUnitsTime,
95  string)) {
96 
97  success = true;
98  value = dt.toTimeSpec (REFERENCE_TIME_ZONE).toTime_t ();
99  iterating = false; // Stop iterating
100 
101  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::dateTimeLookup"
102  << " string=" << string.toLatin1().data()
103  << " qDateTimeFormatMatched=" << formatDateTime.toLatin1().data()
104  << " value=" << value
105  << " stringQDateTime=" << dt.toString().toLatin1().data();
106 
107  }
108  } else {
109 
110  QRegExp reg (formatDateTime);
111  if (reg.exactMatch(string)) {
112 
113  success = true; // Note that value does not get set in QRegExp case
114  iterating = false; // Stop iterating
115 
116  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::dateTimeLookup"
117  << " string=" << string.toLatin1().data()
118  << " regExpMatched=" << formatDateTime.toLatin1().data();
119 
120  }
121  }
122  }
123  }
124  }
125 }
126 
128  CoordUnitsTime coordUnitsTime,
129  double value) const
130 {
131  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::formatOutput"
132  << " value=" << value;
133 
134  ENGAUGE_ASSERT (m_formatsDateFormat.contains (coordUnitsDate));
135  ENGAUGE_ASSERT (m_formatsTimeFormat.contains (coordUnitsTime));
136 
137  QString format = m_formatsDateFormat [coordUnitsDate] + " " + m_formatsTimeFormat [coordUnitsTime];
138  format = format.trimmed();
139 
140  // Range of unsigned versus signed is not a problem here. A signed value maxes out at 2.4 billion
141  // which is year 2038
142  QDateTime dt = QDateTime::fromTime_t (unsigned (qFloor (value)));
143 
144  return dt.toTimeSpec(REFERENCE_TIME_ZONE).toString (format);
145 }
146 
147 void FormatDateTime::loadFormatsFormat()
148 {
149  m_formatsDateFormat [COORD_UNITS_DATE_SKIP] = "";
150  m_formatsDateFormat [COORD_UNITS_DATE_MONTH_DAY_YEAR] = "MM/dd/yyyy";
151  m_formatsDateFormat [COORD_UNITS_DATE_DAY_MONTH_YEAR] = "dd/MM/yyyy";
152  m_formatsDateFormat [COORD_UNITS_DATE_YEAR_MONTH_DAY] = "yyyy/MM/dd";
153 
154  ENGAUGE_ASSERT (m_formatsDateFormat.count () == NUM_COORD_UNITS_DATE);
155 
156  m_formatsTimeFormat [COORD_UNITS_TIME_SKIP] = "";
157  m_formatsTimeFormat [COORD_UNITS_TIME_HOUR_MINUTE] = "hh/mm";
158  m_formatsTimeFormat [COORD_UNITS_TIME_HOUR_MINUTE_SECOND] = "hh:mm:ss";
159 
160  ENGAUGE_ASSERT (m_formatsTimeFormat.count () == NUM_COORD_UNITS_TIME);
161 }
162 
163 void FormatDateTime::loadFormatsParseAcceptable()
164 {
165  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::loadFormatsParseAcceptable";
166 
167  QStringList skip, dayMonth, dayMonthYear, monthDay, monthDayYear, yearMonth, yearMonthDay;
168 
169  // COORD_UNITS_DATE_SKIP and COORD_UNITS_TIME_SKIP allow date/time respectively even when skipped,
170  // although there can be ambiguity with between COORD_UNITS_DATE_MONTH_DAY_YEAR and COORD_UNITS_DATE_DAY_MONTH_YEAR
171  skip << "";
172 
173  dayMonth << "d/M"
174  << "d-M"
175  << "d/MM"
176  << "d-MM"
177  << "d/MMM"
178  << "d-MMM"
179  << "d/MMMM"
180  << "d-MMMM"
181  << "dd/M"
182  << "dd-M"
183  << "dd/M"
184  << "dd-M"
185  << "dd/MM"
186  << "dd-MM"
187  << "dd/MMM"
188  << "dd-MMM"
189  << "dd/MMMM"
190  << "dd-MMMM";
191  dayMonthYear << "d/M/yyyy"
192  << "d-M-yyyy"
193 
194  << "d/MM/yyyy"
195  << "d-MM-yyyy"
196  << "d/MMM/yyyy"
197  << "d-MMM-yyyy"
198  << "d MMM yyyy"
199  << "d/MMMM/yyyy"
200  << "d-MMMM-yyyy"
201  << "d MMMM yyyy"
202 
203  << "dd/MM/yyyy"
204  << "dd-MM-yyyy"
205  << "dd/MMM/yyyy"
206  << "dd-MMM-yyyy"
207  << "dd MMM yyyy"
208  << "dd/MMMM/yyyy"
209  << "dd-MMMM-yyyy"
210  << "dd MMMM yyyy";
211  monthDay << "M/d"
212  << "M-d"
213  << "M d"
214  << "M/dd"
215  << "M-dd"
216  << "M dd"
217  << "MM/d"
218  << "MM-d"
219  << "MM d"
220  << "MM/dd"
221  << "MM-dd"
222  << "MM dd"
223  << "MMM/d"
224  << "MMM-d"
225  << "MMM d"
226  << "MMM/dd"
227  << "MMM-dd"
228  << "MMM dd"
229  << "MMMM/d"
230  << "MMMM-d"
231  << "MMMM d"
232  << "MMMM/dd"
233  << "MMMM-dd"
234  << "MMMM dd";
235  monthDayYear << "M/d/yyyy"
236  << "M-d-yyyy"
237  << "M d yyyy"
238  << "M/dd/yyyy"
239  << "M-dd-yyyy"
240  << "M dd yyyy"
241  << "MM/d/yyyy"
242  << "MM-d-yyyy"
243  << "MM d yyyy"
244  << "MM/dd/yyyy"
245  << "MM-dd-yyyy"
246  << "MM dd yyyy"
247  << "MMM/d/yyyy"
248  << "MMM-d-yyyy"
249  << "MMM d yyyy"
250  << "MMM/dd/yyyy"
251  << "MMM-dd-yyyy"
252  << "MMM dd yyyy"
253  << "MMMM/d/yyyy"
254  << "MMMM-d-yyyy"
255  << "MMMM d"
256  << "MMMM/dd"
257  << "MMMM-dd"
258  << "MMMM dd";
259  yearMonth << "yyyy/M"
260  << "yyyy-M"
261  << "yyyy M"
262  << "yyyy/MM"
263  << "yyyy-MM"
264  << "yyyy MM"
265  << "yyyy/MMM"
266  << "yyyy-MMM"
267  << "yyyy MMM"
268  << "yyyy/MMMM"
269  << "yyyy-MMMM"
270  << "yyyy MMMM";
271  yearMonthDay << "yyyy/M/d"
272  << "yyyy-M-d"
273  << "yyyy M d"
274  << "yyyy/M/dd"
275  << "yyyy-M-dd"
276  << "yyyy M dd"
277  << "yyyy/MM/dd"
278  << "yyyy-MM-dd"
279  << "yyyy MM dd"
280  << "yyyy/MMM/d"
281  << "yyyy-MMM-d"
282  << "yyyy MMM d"
283  << "yyyy/MMM/dd"
284  << "yyyy-MMM-dd"
285  << "yyyy MMM dd"
286  << "yyyy/MMMM/dd"
287  << "yyyy-MMMM-dd"
288  << "yyyy MMMM dd";
289 
290  // Potential day-month ambiguity for COORD_UNITS_DATE_SKIP gets treated as month/day
291  m_formatsDateParseAcceptable [COORD_UNITS_DATE_SKIP] = skip + monthDay + monthDayYear + yearMonthDay;
292  m_formatsDateParseAcceptable [COORD_UNITS_DATE_MONTH_DAY_YEAR] = skip + monthDay + monthDayYear + yearMonthDay;
293  m_formatsDateParseAcceptable [COORD_UNITS_DATE_DAY_MONTH_YEAR] = skip + dayMonth + dayMonthYear + yearMonthDay;
294  m_formatsDateParseAcceptable [COORD_UNITS_DATE_YEAR_MONTH_DAY] = skip + yearMonth + yearMonthDay;
295 
296  ENGAUGE_ASSERT (m_formatsDateParseAcceptable.count () == NUM_COORD_UNITS_DATE);
297 
298  QStringList hour, hourMinute, hourMinuteSecond, hourMinutePm, hourMinuteSecondPm;
299 
300  hour << "hh";
301  hourMinute << "hh:mm";
302  hourMinuteSecond << "hh:mm:ss";
303  hourMinutePm << "hh:mmA"
304  << "hh:mm A"
305  << "hh:mma"
306  << "hh:mm a";
307  hourMinuteSecondPm << "hh:mm:ssA"
308  << "hh:mm:ss A"
309  << "hh:mm:ssa"
310  << "hh:mm:ss a";
311 
312  m_formatsTimeParseAcceptable [COORD_UNITS_TIME_SKIP] = skip + hour + hourMinute + hourMinuteSecond + hourMinutePm + hourMinuteSecondPm;
313  m_formatsTimeParseAcceptable [COORD_UNITS_TIME_HOUR_MINUTE] = skip + hour + hourMinute + hourMinutePm + hourMinuteSecond + hourMinuteSecondPm;
314  m_formatsTimeParseAcceptable [COORD_UNITS_TIME_HOUR_MINUTE_SECOND] = skip + hour + hourMinute + hourMinutePm + hourMinuteSecond + hourMinuteSecondPm;
315 
316  ENGAUGE_ASSERT (m_formatsTimeParseAcceptable.count () == NUM_COORD_UNITS_TIME);
317 }
318 
319 void FormatDateTime::loadFormatsParseIncomplete()
320 {
321  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::loadFormatsParseIncomplete";
322 
323  QStringList skip, day, dayMonth, month, monthDay, monthDayYear, year, yearMonth, yearMonthDay;
324 
325  // COORD_UNITS_DATE_SKIP and COORD_UNITS_TIME_SKIP allow date/time respectively even when skipped,
326  // although there can be ambiguity with between COORD_UNITS_DATE_MONTH_DAY_YEAR and COORD_UNITS_DATE_DAY_MONTH_YEAR
327  skip << "";
328 
329  // IMPORTANT! Be sure to include complete date values since the date, which goes before the time, will be
330  // complete when the time is getting
331  day << "\\d{1,2}"
332  << "\\d{1,2}/"
333  << "\\d{1,2}-";
334  dayMonth << "\\d{1,2}/\\d{1,2}"
335  << "\\d{1,2}/\\d{1,2} "
336  << "\\d{1,2}/\\d{1,2}/"
337  << "\\d{1,2}-\\d{1,2}-"
338  << "\\d{1,2}/[a-zA-Z]{1,12}/"
339  << "\\d{1,2}-[a-zA-Z]{1,12}-"
340  << "\\d{1,2} [a-zA-Z]{1,12} ";
341  month << "\\d{1,2}"
342  << "\\d{1,2}/"
343  << "[a-zA-Z]{1,12}"
344  << "[a-zA-Z]{1,12} ";
345  monthDay << "\\d{1,2}/\\d{1,2}"
346  << "\\d{1,2}/\\d{1,2} "
347  << "\\d{1,2}/\\d{1,2}/"
348  << "\\d{1,2} \\d{1,2}"
349  << "\\d{1,2} \\d{1,2} "
350  << "\\d{1,2}-\\d{1,2}-"
351  << "[a-zA-Z]{1,12}"
352  << "[a-zA-Z]{1,12} "
353  << "[a-zA-Z]{1,12} \\d{1,2}"
354  << "[a-zA-Z]{1,12} \\d{1,2} ";
355  monthDayYear << "\\d{1,2}/\\d{1,2}/\\d{1,4}"
356  << "\\d{1,2}/\\d{1,2}/\\d{1,4} "
357  << "\\d{1,2}-\\d{1,2}-\\d{1,4}"
358  << "\\d{1,2}-\\d{1,2}-\\d{1,4} "
359  << "\\d{1,2} \\d{1,2} \\d{1,4}"
360  << "\\d{1,2} \\d{1,2} \\d{1,4} ";
361  year << "\\d{1,4}"
362  << "\\d{1,4} "
363  << "\\d{1,4}/"
364  << "\\d{1,4}-";
365  yearMonth << "\\d{4}/\\d{1,2}"
366  << "\\d{4}/\\d{1,2} "
367  << "\\d{4}/\\d{1,2}/"
368  << "\\d{4}-\\d{1,2}"
369  << "\\d{4}-\\d{1,2} "
370  << "\\d{4}-\\d{1,2}-"
371  << "\\d{4} \\d{1,2}"
372  << "\\d{4} \\d{1,2} "
373  << "\\d{4}/[a-zA-Z]{1,12}"
374  << "\\d{4}/[a-zA-Z]{1,12} "
375  << "\\d{4}/[a-zA-Z]{1,12}/"
376  << "\\d{4}-[a-zA-Z]{1,12}"
377  << "\\d{4}-[a-zA-Z]{1,12} "
378  << "\\d{4}-[a-zA-Z]{1,12}-"
379  << "\\d{4} [a-zA-Z]{1,12}"
380  << "\\d{4} [a-zA-Z]{1,12} ";
381  yearMonthDay << "\\d{4}/\\d{1,2}/\\d{1,2}"
382  << "\\d{4}/\\d{1,2}-\\d{1,2}"
383  << "\\d{4} \\d{1,2} \\d{1,2}"
384  << "\\d{4}/[a-zA-Z]{1,12}/\\d{1,2}"
385  << "\\d{4}-[a-zA-Z]{1,12}-\\d{1,2}";
386 
387  // For every entry, the possible states leading up to the Acceptable states in m_formatsDateParseIncomplete are all included.
388  // Potential day-month ambiguity for COORD_UNITS_DATE_SKIP gets treated as month/day.
389  m_formatsDateParseIncomplete [COORD_UNITS_DATE_SKIP] = skip + month + monthDay + monthDayYear + year + yearMonth + yearMonthDay;
390  m_formatsDateParseIncomplete [COORD_UNITS_DATE_MONTH_DAY_YEAR] = skip + month + monthDay + monthDayYear + year + yearMonth + yearMonthDay;
391  m_formatsDateParseIncomplete [COORD_UNITS_DATE_DAY_MONTH_YEAR] = skip + day + dayMonth + year + yearMonth + yearMonthDay;
392  m_formatsDateParseIncomplete [COORD_UNITS_DATE_YEAR_MONTH_DAY] = skip + year + yearMonth + yearMonthDay;
393 
394  ENGAUGE_ASSERT (m_formatsDateParseIncomplete.count () == NUM_COORD_UNITS_DATE);
395 
396  QStringList hour, hourMinute, hourMinuteAmPm, hourMinuteSecond, hourMinuteSecondAmPm;
397 
398  hour << "\\d{1,2}"
399  << "\\d{1,2}:";
400  hourMinute << "\\d{1,2}:\\d{1,2}"
401  << "\\d{1,2}:\\d{1,2}:"
402  << "\\d{1,2}:\\d{1,2} ";
403  hourMinuteAmPm << "\\d{1,2}:\\d{1,2} [aApP]";
404  hourMinuteSecond << "\\d{1,2}:\\d{1,2}:\\d{1,2}"
405  << "\\d{1,2}:\\d{1,2}:\\d{1,2} ";
406  hourMinuteSecondAmPm << "\\d{1,2}:\\d{1,2}:\\d{1,2} [aApP]";
407 
408  // For every entry, the possible states leading up to the Acceptable states in m_formatsTimeParseIncomplete are all included.
409  m_formatsTimeParseIncomplete [COORD_UNITS_TIME_SKIP] = skip +
410  hour +
411  hourMinute + hourMinuteAmPm +
412  hourMinuteSecond + hourMinuteSecondAmPm;
413  m_formatsTimeParseIncomplete [COORD_UNITS_TIME_HOUR_MINUTE] = skip +
414  hour +
415  hourMinute + hourMinuteAmPm +
416  hourMinuteSecond + hourMinuteSecondAmPm;
417  m_formatsTimeParseIncomplete [COORD_UNITS_TIME_HOUR_MINUTE_SECOND] = skip +
418  hour +
419  hourMinute + hourMinuteAmPm +
420  hourMinuteSecond + hourMinuteSecondAmPm;
421 
422  ENGAUGE_ASSERT (m_formatsTimeParseIncomplete.count () == NUM_COORD_UNITS_TIME);
423 }
424 
425 QValidator::State FormatDateTime::parseInput (CoordUnitsDate coordUnitsDate,
426  CoordUnitsTime coordUnitsTime,
427  const QString &stringUntrimmed,
428  double &value) const
429 {
430  LOG4CPP_INFO_S ((*mainCat)) << "FormatDateTime::parseInput"
431  << " date=" << coordUnitsDateToString (coordUnitsDate).toLatin1().data()
432  << " time=" << coordUnitsTimeToString (coordUnitsTime).toLatin1().data()
433  << " string=" << stringUntrimmed.toLatin1().data();
434 
435  const bool USE_QREGEXP = true, DO_NOT_USE_QREGEXP = false;
436 
437  const QString string = stringUntrimmed.trimmed();
438 
439  QValidator::State state;
440  if (string.isEmpty()) {
441 
442  state = QValidator::Intermediate;
443 
444  } else {
445 
446  state = QValidator::Invalid;
447 
448  // First see if value is acceptable
449  bool success = false;
450  dateTimeLookup (m_formatsDateParseAcceptable,
451  m_formatsTimeParseAcceptable,
452  coordUnitsDate,
453  coordUnitsTime,
454  string,
455  USE_QREGEXP,
456  value,
457  success);
458  if (success) {
459 
460  state = QValidator::Acceptable;
461 
462  } else {
463 
464  // Not acceptable, but perhaps it is just incomplete
465  dateTimeLookup (m_formatsDateParseIncomplete,
466  m_formatsTimeParseIncomplete,
467  coordUnitsDate,
468  coordUnitsTime,
469  string,
470  DO_NOT_USE_QREGEXP,
471  value,
472  success);
473  if (success) {
474 
475  state = QValidator::Intermediate;
476 
477  }
478  }
479  }
480 
481  return state;
482 }
FormatDateTime()
Single constructor.
QHash< CoordUnitsDate, QStringList > FormatsDate
#define LOG4CPP_INFO_S(logger)
Definition: convenience.h:18
QString coordUnitsDateToString(CoordUnitsDate coordUnits)
QHash< CoordUnitsTime, QStringList > FormatsTime
const Qt::TimeSpec REFERENCE_TIME_ZONE(Qt::UTC)
log4cpp::Category * mainCat
Definition: Logger.cpp:14
CoordUnitsTime
QValidator::State parseInput(CoordUnitsDate coordUnitsDate, CoordUnitsTime coordUnitsTime, const QString &stringUntrimmed, double &value) const
Parse the input string into a time value.
CoordUnitsDate
QString coordUnitsTimeToString(CoordUnitsTime coordUnits)
#define ENGAUGE_ASSERT(cond)
Drop in replacement for Q_ASSERT if defined(QT_NO_DEBUG) && !defined(QT_FORCE_ASSERTS) define ENGAUGE...
Definition: EngaugeAssert.h:20
QString formatOutput(CoordUnitsDate coordUnitsDate, CoordUnitsTime coordUnitsTime, double value) const
Format the date/time value according to date/time format settings.