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