alkimia  8.0.3
alkdateformat.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * Copyright 2004 Ace Jones <acejones@users.sourceforge.net> *
3  * Copyright 2018-2019 Thomas Baumgart <tbaumgart@kde.org> *
4  * *
5  * This file is part of libalkimia. *
6  * *
7  * libalkimia is free software; you can redistribute it and/or *
8  * modify it under the terms of the GNU General Public License *
9  * as published by the Free Software Foundation; either version 2.1 of *
10  * the License or (at your option) version 3 or any later version. *
11  * *
12  * libalkimia is distributed in the hope that it will be useful, *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15  * GNU General Public License for more details. *
16  * *
17  * You should have received a copy of the GNU General Public License *
18  * along with this program. If not, see <http://www.gnu.org/licenses/> *
19  ***************************************************************************/
20 
21 #include "alkdateformat.h"
22 
23 #include <QDebug>
24 
25 
26 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
27 #include <KGlobal>
28 #include <KCalendarSystem>
29 #else
30 #include <QLocale>
31 #include <QRegularExpression>
32 #include <QRegularExpressionMatch>
33 #endif
34 
35 
37 {
38 public:
39  QString m_format;
41  QString m_errorMessage;
42 
43  QDate setError(AlkDateFormat::ErrorCode errorCode, const QString& arg1 = QString(), const QString& arg2 = QString())
44  {
45  m_errorCode = errorCode;
46  switch(errorCode) {
48  m_errorMessage.clear();
49  break;
51  m_errorMessage = QString("Invalid format string '%1'").arg(arg1);
52  break;
54  m_errorMessage = QString("Invalid format character '%1'").arg(arg1);
55  break;
57  m_errorMessage = QString("Invalid date '%1'").arg(arg1);
58  break;
60  m_errorMessage = QString("Invalid day entry: %1").arg(arg1);
61  break;
63  m_errorMessage = QString("Invalid month entry: %1").arg(arg1);
64  break;
66  m_errorMessage = QString("Invalid year entry: %1").arg(arg1);
67  break;
69  m_errorMessage = QString("Length of year (%1) does not match expected length (%2).").arg(arg1, arg2);
70  break;
71  }
72  return QDate();
73  }
74 
75  QDate convertStringSkrooge(const QString &_in)
76  {
77  QDate date;
78  if (m_format == "UNIX") {
79 #if QT_VERSION >= QT_VERSION_CHECK(5,8,0)
80  date = QDateTime::fromSecsSinceEpoch(_in.toUInt(), Qt::UTC).date();
81 #else
82  date = QDateTime::fromTime_t(_in.toUInt()).date();
83 #endif
84  } else {
85  const QString skroogeFormat = m_format;
86 
87  m_format = m_format.toLower();
88 
89  QRegExp formatrex("([mdy]+)(\\W+)([mdy]+)(\\W+)([mdy]+)", Qt::CaseInsensitive);
90  if (formatrex.indexIn(m_format) == -1) {
92  }
93  m_format = QLatin1String("%");
94  m_format.append(formatrex.cap(1));
95  m_format.append(formatrex.cap(2));
96  m_format.append(QLatin1String("%"));
97  m_format.append(formatrex.cap(3));
98  m_format.append(formatrex.cap(4));
99  m_format.append(QLatin1String("%"));
100  m_format.append(formatrex.cap(5));
101 
102  date = convertStringKMyMoney(_in, true, 2000);
103  m_format = skroogeFormat;
104  }
105  if (!date.isValid()) {
107  }
108  if (!m_format.contains(QStringLiteral("yyyy")) && date.year() < 2000)
109  date = date.addYears(100);
110  return date;
111  }
112 
113 
114 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
115 
116  QDate convertStringKMyMoney(const QString &_in, bool _strict, unsigned _centurymidpoint)
117  {
118  //
119  // Break date format string into component parts
120  //
121 
122  QRegExp formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", Qt::CaseInsensitive);
123  if (formatrex.indexIn(m_format) == -1) {
125  }
126 
127  QStringList formatParts;
128  formatParts += formatrex.cap(1);
129  formatParts += formatrex.cap(3);
130  formatParts += formatrex.cap(5);
131 
132  QStringList formatDelimiters;
133  formatDelimiters += formatrex.cap(2);
134  formatDelimiters += formatrex.cap(4);
135 
136  // make sure to escape delimiters that are special chars in regex
137  QStringList::iterator it;
138  QRegExp specialChars("^[\\.\\\\\\?]$");
139  for(it = formatDelimiters.begin(); it != formatDelimiters.end(); ++it) {
140  if (specialChars.indexIn(*it) != -1)
141  (*it).prepend("\\");
142  }
143 
144  //
145  // Break input string up into component parts,
146  // using the delimiters found in the format string
147  //
148 
149  QRegExp inputrex;
150  inputrex.setCaseSensitivity(Qt::CaseInsensitive);
151 
152  // strict mode means we must enforce the delimiters as specified in the
153  // format. non-strict allows any delimiters
154  if (_strict) {
155  inputrex.setPattern(QString("(\\w+)\\.?%1(\\w+)\\.?%2(\\w+)\\.?").arg(formatDelimiters[0],
156  formatDelimiters[1]));
157  } else {
158  inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)");
159  }
160 
161  if (inputrex.indexIn(_in) == -1) {
163  }
164 
165  QStringList scannedParts;
166  scannedParts += inputrex.cap(1).toLower();
167  scannedParts += inputrex.cap(2).toLower();
168  scannedParts += inputrex.cap(3).toLower();
169 
170  //
171  // Convert the scanned parts into actual date components
172  //
173  unsigned day = 0, month = 0, year = 0;
174  bool ok;
175  QRegExp digitrex("(\\d+)");
176  QStringList::const_iterator it_scanned = scannedParts.constBegin();
177  QStringList::const_iterator it_format = formatParts.constBegin();
178  while (it_scanned != scannedParts.constEnd()) {
179  // decide upon the first character of the part
180  switch ((*it_format).at(0).cell()) {
181  case 'd':
182  // remove any extraneous non-digits (e.g. read "3rd" as 3)
183  ok = false;
184  if (digitrex.indexIn(*it_scanned) != -1) {
185  day = digitrex.cap(1).toUInt(&ok);
186  }
187  if (!ok || day > 31) {
188  return setError(AlkDateFormat::InvalidDay, *it_scanned);
189  }
190  break;
191  case 'm':
192  month = (*it_scanned).toUInt(&ok);
193  if (!ok) {
194  // maybe it's a textual date
195  unsigned i = 1;
196  while (i <= 12) {
197  if (KGlobal::locale()->calendar()->monthName(i, 2000).toLower() == *it_scanned
198  || KGlobal::locale()->calendar()->monthName(i, 2000,
199  KCalendarSystem::ShortName).
200  toLower() == *it_scanned) {
201  month = i;
202  }
203  ++i;
204  }
205  }
206 
207  if (month < 1 || month > 12) {
208  return setError(AlkDateFormat::InvalidMonth, *it_scanned);
209  }
210 
211  break;
212  case 'y':
213  if (_strict && (*it_scanned).length() != (*it_format).length()) {
214  return setError(AlkDateFormat::InvalidYearLength, *it_scanned, *it_format);
215  }
216 
217  year = (*it_scanned).toUInt(&ok);
218 
219  if (!ok) {
220  return setError(AlkDateFormat::InvalidYear, *it_scanned);
221  }
222 
223  //
224  // 2-digit year case
225  //
226  // this algorithm will pick a year within +/- 50 years of the
227  // centurymidpoint parameter. i.e. if the midpoint is 2000,
228  // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999
229  if (year < 100) {
230  unsigned centuryend = _centurymidpoint + 50;
231  unsigned centurybegin = _centurymidpoint - 50;
232 
233  if (year < centuryend % 100) {
234  year += 100;
235  }
236  year += centurybegin - centurybegin % 100;
237  }
238 
239  if (year < 1900) {
240  return setError(AlkDateFormat::InvalidYear, QString::number(year));
241  }
242 
243  break;
244  default:
245  return setError(AlkDateFormat::InvalidFormatCharacter, QString((*it_format).at(0).cell()));
246  }
247 
248  ++it_scanned;
249  ++it_format;
250  }
251  QDate result(year, month, day);
252  if (!result.isValid()) {
253  return setError(AlkDateFormat::InvalidDate, QString("yr:%1 mo:%2 dy:%3)").arg(year).arg(month).arg(day));
254  }
255 
256  return result;
257  }
258 
259 #else // Qt5
260 
261  QDate convertStringKMyMoney(const QString& _in, bool _strict, unsigned _centurymidpoint)
262  {
263  //
264  // Break date format string into component parts
265  //
266 
267  QRegularExpression formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)", QRegularExpression::CaseInsensitiveOption);
268  QRegularExpressionMatch match = formatrex.match(m_format);
269  if (!match.hasMatch()) {
271  }
272 
273  QStringList formatParts;
274  formatParts += match.captured(1);
275  formatParts += match.captured(3);
276  formatParts += match.captured(5);
277 
278  QStringList formatDelimiters;
279  formatDelimiters += match.captured(2);
280  formatDelimiters += match.captured(4);
281 
282  // make sure to escape delimiters that are special chars in regex
283  QStringList::iterator it;
284  QRegularExpression specialChars("^[\\.\\\\\\?]$");
285  for(it = formatDelimiters.begin(); it != formatDelimiters.end(); ++it) {
286  QRegularExpressionMatch special = specialChars.match(*it);
287  if (special.hasMatch()) {
288  (*it).prepend("\\");
289  }
290  }
291 
292  //
293  // Break input string up into component parts,
294  // using the delimiters found in the format string
295  //
296  QRegularExpression inputrex;
297  inputrex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
298 
299  // strict mode means we must enforce the delimiters as specified in the
300  // format. non-strict allows any delimiters
301  if (_strict)
302  inputrex.setPattern(QString("(\\w+)\\.?%1(\\w+)\\.?%2(\\w+)\\.?").arg(formatDelimiters[0], formatDelimiters[1]));
303  else
304  inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)");
305 
306  match = inputrex.match(_in);
307  if (!match.hasMatch()) {
309  }
310 
311  QStringList scannedParts;
312  scannedParts += match.captured(1).toLower();
313  scannedParts += match.captured(2).toLower();
314  scannedParts += match.captured(3).toLower();
315 
316  //
317  // Convert the scanned parts into actual date components
318  //
319  unsigned day = 0, month = 0, year = 0;
320  bool ok;
321  QRegularExpression digitrex("(\\d+)");
322  QStringList::const_iterator it_scanned = scannedParts.constBegin();
323  QStringList::const_iterator it_format = formatParts.constBegin();
324  while (it_scanned != scannedParts.constEnd()) {
325  // decide upon the first character of the part
326  switch ((*it_format).at(0).cell()) {
327  case 'd':
328  // remove any extraneous non-digits (e.g. read "3rd" as 3)
329  ok = false;
330  match = digitrex.match(*it_scanned);
331  if (match.hasMatch())
332  day = match.captured(1).toUInt(&ok);
333  if (!ok || day > 31)
334  return setError(AlkDateFormat::InvalidDay, *it_scanned);
335  break;
336  case 'm':
337  month = (*it_scanned).toUInt(&ok);
338  if (!ok) {
339  month = 0;
340  // maybe it's a textual date
341  unsigned i = 1;
342  // search the name in the current selected locale
343  QLocale locale;
344  while (i <= 12) {
345  if (locale.standaloneMonthName(i).toLower() == *it_scanned
346  || locale.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
347  month = i;
348  break;
349  }
350  ++i;
351  }
352  // in case we did not find the month in the current locale,
353  // we look for it in the C locale
354  if(month == 0) {
355  QLocale localeC(QLocale::C);
356  if( !(locale == localeC)) {
357  i = 1;
358  while (i <= 12) {
359  if (localeC.standaloneMonthName(i).toLower() == *it_scanned
360  || localeC.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) {
361  month = i;
362  break;
363  }
364  ++i;
365  }
366  }
367  }
368  }
369 
370  if (month < 1 || month > 12)
371  return setError(AlkDateFormat::InvalidMonth, *it_scanned);
372 
373  break;
374  case 'y':
375  if (_strict && (*it_scanned).length() != (*it_format).length())
376  return setError(AlkDateFormat::InvalidYearLength, *it_scanned, *it_format);
377 
378  year = (*it_scanned).toUInt(&ok);
379 
380  if (!ok)
381  return setError(AlkDateFormat::InvalidYear, *it_scanned);
382 
383  //
384  // 2-digit year case
385  //
386  // this algorithm will pick a year within +/- 50 years of the
387  // centurymidpoint parameter. i.e. if the midpoint is 2000,
388  // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999
389  if (year < 100) {
390  unsigned centuryend = _centurymidpoint + 50;
391  unsigned centurybegin = _centurymidpoint - 50;
392 
393  if (year < centuryend % 100)
394  year += 100;
395  year += centurybegin - centurybegin % 100;
396  }
397 
398  if (year < 1900)
399  return setError(AlkDateFormat::InvalidYear, QString::number(year));
400 
401  break;
402  default:
403  return setError(AlkDateFormat::InvalidFormatCharacter, QString((*it_format).at(0).cell()));
404  }
405 
406  ++it_scanned;
407  ++it_format;
408  }
409  QDate result(year, month, day);
410  if (! result.isValid())
411  return setError(AlkDateFormat::InvalidDate, QString("yr:%1 mo:%2 dy:%3)").arg(year).arg(month).arg(day));
412 
413  return result;
414  }
415 #endif
416 
417 };
418 
419 AlkDateFormat::AlkDateFormat(const QString &format)
420  : d(new Private)
421 {
422  d->m_format = format;
423  d->m_errorCode = NoError;
424 }
425 
427 {
428  delete d;
429 }
430 
432 {
433  d->m_format = right.d->m_format;
434 
435  return *this;
436 }
437 
438 const QString & AlkDateFormat::format() const
439 {
440  return d->m_format;
441 }
442 
444 {
445  return d->m_errorCode;
446 }
447 
449 {
450  return d->m_errorMessage;
451 }
452 
453 
454 QDate AlkDateFormat::convertString(const QString& date, bool strict, unsigned int centuryMidPoint)
455 {
456  // reset any pending errors from previous runs
457  d->m_errorCode = NoError;
458  d->m_errorMessage.clear();
459 
460  if (d->m_format.contains("%"))
461  return d->convertStringKMyMoney(date, strict, centuryMidPoint);
462  else
463  return d->convertStringSkrooge(date);
464 }
465 
466 QString AlkDateFormat::convertDate(const QDate& date)
467 {
468  Q_UNUSED(date);
469 
470  // reset any pending errors from previous runs
471  d->m_errorCode = NoError;
472  d->m_errorMessage.clear();
473 
474  return QString();
475 }
AlkDateFormat::Private::setError
QDate setError(AlkDateFormat::ErrorCode errorCode, const QString &arg1=QString(), const QString &arg2=QString())
Definition: alkdateformat.cpp:61
AlkDateFormat::Private
Definition: alkdateformat.cpp:37
AlkDateFormat::Private::m_errorMessage
QString m_errorMessage
Definition: alkdateformat.cpp:59
AlkDateFormat::convertDate
QString convertDate(const QDate &date)
Definition: alkdateformat.cpp:466
AlkDateFormat::ErrorCode
ErrorCode
Definition: alkdateformat.h:64
AlkDateFormat::Private::m_errorCode
AlkDateFormat::ErrorCode m_errorCode
Definition: alkdateformat.cpp:58
AlkDateFormat::Private::m_format
QString m_format
Definition: alkdateformat.cpp:57
AlkDateFormat::NoError
@ NoError
Definition: alkdateformat.h:65
alkdateformat.h
AlkDateFormat::lastErrorMessage
QString lastErrorMessage() const
Definition: alkdateformat.cpp:448
AlkDateFormat::InvalidYearLength
@ InvalidYearLength
Definition: alkdateformat.h:72
AlkDateFormat::AlkDateFormat
AlkDateFormat(const QString &format)
Definition: alkdateformat.cpp:419
AlkDateFormat::InvalidFormatString
@ InvalidFormatString
Definition: alkdateformat.h:66
AlkDateFormat::InvalidFormatCharacter
@ InvalidFormatCharacter
Definition: alkdateformat.h:67
AlkDateFormat::InvalidDate
@ InvalidDate
Definition: alkdateformat.h:68
AlkDateFormat::lastError
ErrorCode lastError() const
Definition: alkdateformat.cpp:443
AlkDateFormat::Private::convertStringSkrooge
QDate convertStringSkrooge(const QString &_in)
Definition: alkdateformat.cpp:93
AlkDateFormat::convertString
QDate convertString(const QString &date, bool strict=true, unsigned centuryMidPoint=QDate::currentDate().year())
Definition: alkdateformat.cpp:454
AlkDateFormat::InvalidMonth
@ InvalidMonth
Definition: alkdateformat.h:70
AlkDateFormat::format
const QString & format() const
Definition: alkdateformat.cpp:438
AlkDateFormat::InvalidYear
@ InvalidYear
Definition: alkdateformat.h:71
AlkDateFormat::d
Private *const d
Definition: alkdateformat.h:84
AlkDateFormat::~AlkDateFormat
~AlkDateFormat()
Definition: alkdateformat.cpp:426
AlkDateFormat
Definition: alkdateformat.h:40
AlkDateFormat::operator=
AlkDateFormat & operator=(const AlkDateFormat &)
Definition: alkdateformat.cpp:431
AlkDateFormat::Private::convertStringKMyMoney
QDate convertStringKMyMoney(const QString &_in, bool _strict, unsigned _centurymidpoint)
Definition: alkdateformat.cpp:279
AlkDateFormat::InvalidDay
@ InvalidDay
Definition: alkdateformat.h:69