Skip to content

Commit

Permalink
Extend max year from 4000 to 9999 in timestamps
Browse files Browse the repository at this point in the history
Probleme detecte par Nicolas:
- les dates ou timestamps avec annee 9999 ne sont pas reconnus comme format valide, par le detecteur de format
- c'est normal: la limite dans Khiops 10.1 est l'an 4000, depuis la prise en compte des timezone dans les timestamps

Solution fonctionnelle:
- etendre a nouveau la limite des annees a 9999
- en diminuant legerement la precsiiobn des fraction de scondes (et 1/10000 a 1/1000)
- meilleurs compromis utilisateur
  - moins d'anomalies dans la lecture de bases existantes
    - le 9999 est souvent utilise pour recoder les valeurs manquantes
    - il est important de detecter correctement les format timestamps dans ce cas
  - tout en gardant un precision de une millisconde
    - perdre le dixieme de millisecondes est peu important
    - si on veut passer a plus fin, ce sera la micro-secondes
      - besoin peu frequent
      - necessitera de toute facon a un autre format (variable numerique directement, probablement)

Impact dans la documentation utilisateur, a prendre en compte

Implementation existante
- les classe Date et Time etait chacune codee avec un int, et timestamp avec un longint concatenant les deux
- ce n'est plus possible, car il faut recuperer 2 bits de Time pour les utiliser dans Date

Nouveau type interne de type union: DataTime
- type interne, utilise par toutes les classe Date, Time, Timestamp, TimestampTZ
- defini une union permetant des manipulation simple de tout ou partie des champs
  - lBytes: longint, compatible avec KWValue, pour acceder a tout en une seule operation
  - parts: bit fields pour acces a la sous-partie Data, Timezone, Time
  - fields: bit fields pour acces a tous les champs elementaires
- attention, contrainte d'alignement d'une struct bit fields sur des int
  - les champs de la timezone etant a cheval sur les deux entiers ont du etre decoupes
  - accesseurs specifiques uniquement pour les champs concernes de la timezone
- bascule vers la limite a 9999 pour les annees
  - ajout de 2 bits pour les anneee, en en recupernt 2 des fractions de secondes
- methode Test, permettant de verifier les contraintes internes de l'union

Utilisation systematique de l'union DateTime dans les type Date, Time, Timestamp, TimestampTZ
- ok avec tous les tests unitaires
- validation sur LearningTest\TestKhiops\Rules\DateTimeRules
- tests complets sur LearningTest
  • Loading branch information
marcboulle committed Nov 28, 2023
1 parent 8fe164e commit ea3466b
Show file tree
Hide file tree
Showing 14 changed files with 427 additions and 203 deletions.
1 change: 1 addition & 0 deletions src/Learning/KWData/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ add_library(
KWSymbol.cpp
KWTextService.cpp
KWTextTokenizer.cpp
KWDateTime.cpp
KWTime.cpp
KWTimestamp.cpp
KWTimestampTZ.cpp
Expand Down
2 changes: 1 addition & 1 deletion src/Learning/KWData/KWDatabaseFormatDetector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,7 @@ int KWDatabaseFormatDetector::ComputeSeparatorPriority(char cSeparator) const
// Recherche de la position dans les separateurs preferes
nPriority = sPreferredSeparators.Find(cSeparator);

// Si non trouve, on prend le le cracater lui meme d'abord dans sa plage ascii, puis dans la plage ascii etendue
// Si non trouve, on prend le le caractere lui meme d'abord dans sa plage ascii, puis dans la plage ascii etendue
if (nPriority == -1)
{
if (isprint(cSeparator))
Expand Down
6 changes: 3 additions & 3 deletions src/Learning/KWData/KWDate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ boolean Date::Init(int nYear, int nMonth, int nDay)
// Initialisation a invalide
Reset();

// L'annee doit etre comprise entre 1 et 4000
if (nYear < 1 or nYear > 4000)
// L'annee doit etre valide
if (nYear < 1 or nYear > DateTime::nMaxYear)
bOk = false;
// Le mois doit etre compris entre 1 et 12
else if (nMonth < 1 or nMonth > 12)
Expand Down Expand Up @@ -192,7 +192,7 @@ boolean Date::AddDays(int nValue)
nYear = e / 1461 - 4716 + (14 - nMonth) / 12;

// Initialisation si annee valide
if (1 <= nYear and nYear <= 4000)
if (1 <= nYear and nYear <= DateTime::nMaxYear)
{
SetYear(nYear);
SetMonth(nMonth);
Expand Down
89 changes: 35 additions & 54 deletions src/Learning/KWData/KWDate.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Date;
class KWDateFormat;

#include "Object.h"
#include "KWDateTime.h"
#include "Vector.h"

////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -28,7 +29,7 @@ class Date : public SystemObject
void Reset();

// Initialisation
// Renvoie true si l'initialisation est valide (entre 01/01/0001 et 31/12/4000)
// Renvoie true si l'initialisation est valide (entre 01/01/0001 et 31/12/9999)
boolean Init(int nYear, int nMonth, int nDay);

// Operateurs de comparaison
Expand Down Expand Up @@ -113,6 +114,7 @@ class Date : public SystemObject
///////////////////////////////////////////////////////////////////////////////////////////////
// La date peut stocker une partie time zone, utile quand elle est utilisee par les TimestampTZ
// qui doivent gerer les time zones
friend class Timestamp;
friend class TimestampTZ;
friend class KWTimestampTZFormat;

Expand Down Expand Up @@ -146,30 +148,8 @@ class Date : public SystemObject
// Affichage de la partie time zone au format zzzzzz si bExtended vaut true, zzzzz sinon
const char* const TimeZoneToString(boolean bExtended) const;

// Stockage optimise d'une Date sous forme d'un entier
// On utilise un champ de bits pour stocker a la fois les information de Date et de time zone, utilises
// potentiellement dans les cas des TimestampTZ
// Bits 1 a 12: annee (entre 0 et 4000)
// Bits 13 a 16: mois (entre 1 et 12)
// Bits 17 a 21: jour (entre 1 et 31, ou 0 si invalide)
// Bit 22: signe de la timezone (0 si negatif, 1 si positif)
// Bits 23 a 26: heure de la timezone (entre 0 et 14)
// Bits 27 a 32: minute de la timezone (entre 0 et 59)
// Une time est invalide si elle vaut 0 (heure a 0; validite de 1 a 24), et interdite si elle vaut 0xFFFFFFFF
// La partie time zone est invalide si elle vaut 0 (une time zone -00:00 est codee +00:00 pour etre valide).
union DateValue
{
unsigned int nDate;
struct DateFields
{
unsigned int nYear : 12;
unsigned int nMonth : 4;
unsigned int nDay : 5;
unsigned int nTimeZoneSign : 1;
unsigned int nTimeZoneHour : 4;
unsigned int nTimeZoneMinute : 6;
} dateFields;
} dateValue;
// Utilisation d'une union DateTime pour acceder au champs Date, dans une structure commune aux type temporels
union DateTime dateValue;
};

// Ecriture dans un stream
Expand Down Expand Up @@ -291,41 +271,41 @@ class KWDateFormat : public Object

inline void Date::Reset()
{
dateValue.nDate = 0;
dateValue.lBytes = 0;
}

inline boolean Date::operator==(const Date& dtValue) const
{
return (dateValue.nDate == dtValue.dateValue.nDate);
return (dateValue.lBytes == dtValue.dateValue.lBytes);
}

inline boolean Date::operator!=(const Date& dtValue) const
{
return (dateValue.nDate != dtValue.dateValue.nDate);
return (dateValue.lBytes != dtValue.dateValue.lBytes);
}

inline boolean Date::Check() const
{
require(not IsForbiddenValue());
return dateValue.nDate != 0;
return dateValue.lBytes != 0;
}

inline int Date::GetYear() const
{
require(Check());
return dateValue.dateFields.nYear;
return dateValue.fields.nYear;
}

inline int Date::GetMonth() const
{
require(Check());
return dateValue.dateFields.nMonth;
return dateValue.fields.nMonth;
}

inline int Date::GetDay() const
{
require(Check());
return dateValue.dateFields.nDay;
return dateValue.fields.nDay;
}

inline int Date::Diff(const Date dtOtherDate) const
Expand Down Expand Up @@ -355,30 +335,30 @@ inline int Date::Compare(const Date dtOtherDate) const

inline void Date::SetYear(int nValue)
{
require(1 <= nValue and nValue <= 4000);
dateValue.dateFields.nYear = nValue;
require(1 <= nValue and nValue <= DateTime::nMaxYear);
dateValue.fields.nYear = nValue;
}

inline void Date::SetMonth(int nValue)
{
require(1 <= nValue and nValue <= 12);
dateValue.dateFields.nMonth = nValue;
dateValue.fields.nMonth = nValue;
}

inline void Date::SetDay(int nValue)
{
require(1 <= nValue and nValue <= 31);
dateValue.dateFields.nDay = nValue;
dateValue.fields.nDay = nValue;
}

inline void Date::SetForbiddenValue()
{
dateValue.nDate = 0xFFFFFFFF;
dateValue.lBytes = DateTime::lForbiddenValue;
}

inline boolean Date::IsForbiddenValue() const
{
return (dateValue.nDate == 0xFFFFFFFF);
return (dateValue.lBytes == DateTime::lForbiddenValue);
}

inline boolean Date::InitTimeZone(int nSign, int nHour, int nMinute)
Expand Down Expand Up @@ -410,19 +390,23 @@ inline boolean Date::InitTimeZone(int nSign, int nHour, int nMinute)

inline boolean Date::SetTimeZoneTotalMinutes(int nValue)
{
int nSign;
int nHour;
int nMinute;

// Initialisation de la timezone a invalide
ResetTimeZone();

// Initialisation si time zone valide
if (-12 * 60 <= nValue and nValue <= 14 * 60)
{
if (nValue >= 0)
SetTimeZoneSign(1);
nSign = 1;
else
SetTimeZoneSign(-1);
SetTimeZoneHour(abs(nValue) / 60);
SetTimeZoneMinute(abs(nValue) % 60);
return true;
nSign = -1;
nHour = abs(nValue) / 60;
nMinute = abs(nValue) % 60;
return InitTimeZone(nSign, nHour, nMinute);
}
return false;
}
Expand All @@ -436,51 +420,48 @@ inline int Date::GetTimeZoneTotalMinutes() const
inline void Date::SetTimeZoneSign(int nValue)
{
require(nValue == -1 or nValue == 1);
dateValue.dateFields.nTimeZoneSign = (nValue + 1) / 2;
dateValue.fields.nTimeZoneSign = (nValue + 1) / 2;
}

inline int Date::GetTimeZoneSign() const
{
require(CheckTimeZone());
return (dateValue.dateFields.nTimeZoneSign * 2) - 1;
return (dateValue.fields.nTimeZoneSign * 2) - 1;
}

inline void Date::SetTimeZoneHour(int nValue)
{
require(0 <= nValue and nValue <= 14);
dateValue.dateFields.nTimeZoneHour = nValue;
dateValue.fields.nTimeZoneHour = nValue;
}

inline int Date::GetTimeZoneHour() const
{
require(CheckTimeZone());
return dateValue.dateFields.nTimeZoneHour;
return dateValue.fields.nTimeZoneHour;
}

inline void Date::SetTimeZoneMinute(int nValue)
{
require(0 <= nValue and nValue <= 59);
dateValue.dateFields.nTimeZoneMinute = nValue;
dateValue.SetTimeZoneMinute(nValue);
}

inline int Date::GetTimeZoneMinute() const
{
require(CheckTimeZone());
return dateValue.dateFields.nTimeZoneMinute;
return dateValue.GetTimeZoneMinute();
}

inline void Date::ResetTimeZone()
{
dateValue.dateFields.nTimeZoneSign = 0;
dateValue.dateFields.nTimeZoneHour = 0;
dateValue.dateFields.nTimeZoneMinute = 0;
dateValue.SetTimeZone(0);
}

inline boolean Date::CheckTimeZone() const
{
require(not IsForbiddenValue());
return dateValue.dateFields.nTimeZoneSign != 0 or dateValue.dateFields.nTimeZoneHour != 0 or
dateValue.dateFields.nTimeZoneMinute != 0;
return dateValue.GetTimeZone() != 0;
}

// KWDateFormat
Expand Down
118 changes: 118 additions & 0 deletions src/Learning/KWData/KWDateTime.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) 2023 Orange. All rights reserved.
// This software is distributed under the BSD 3-Clause-clear License, the text of which is available
// at https://spdx.org/licenses/BSD-3-Clause-Clear.html or see the "LICENSE" file for more details.

#include "KWDateTime.h"

void DateTime::Test()
{
DateTime dtDateTimeValue;
DateTime dtForbiddenValue;
DateTime dtDateValue;
DateTime dtTimezoneValue;
DateTime dtTimeValue;
int i;

// Taille de la structures
cout << "DateTime: " << sizeof(DateTime) << "\n";
assert(sizeof(DateTime) == sizeof(longint));

// Nombre de digits des fractions de scondes
cout << "Frac seconds digits: " << nFracSecondsDigitNumber << "\n";
cout << "Frac seconds max: " << nMaxFracSeconds << "\n";
assert(10 * (nMaxFracSeconds / 10) == nMaxFracSeconds);
assert(int(pow(10, nFracSecondsDigitNumber)) == nMaxFracSeconds),

// Contenu a 0
dtDateTimeValue.lBytes = 0;
cout << "\nDateTime invalid value\n", dtDateTimeValue.WriteInternalFields(cout);

// Contenu interdit
dtForbiddenValue.lBytes = lForbiddenValue;
cout << "\nDateTime forbidden value\n";
dtForbiddenValue.WriteInternalFields(cout);

// Test de mis a jour partielle de la Date
dtDateValue.lBytes = dtForbiddenValue.lBytes;
dtDateValue.parts.nTime = 0;
dtDateValue.SetTimeZone(0);
cout << "\nDate only forbidden value\n";
dtDateValue.WriteInternalFields(cout);

// Test de mis a jour partielle de la Timezone
dtTimezoneValue.lBytes = dtForbiddenValue.lBytes;
dtTimezoneValue.parts.nDate = 0;
dtTimezoneValue.parts.nTime = 0;
cout << "\nTimezone only forbidden value\n";
dtTimezoneValue.WriteInternalFields(cout);

// Test de mis a jour partielle de la Time
dtTimeValue.lBytes = dtForbiddenValue.lBytes;
dtTimeValue.parts.nDate = 0;
dtTimeValue.SetTimeZone(0);
cout << "\nTime only forbidden value\n";
dtTimeValue.WriteInternalFields(cout);

// Verifications avancees
assert((dtDateValue.lBytes | dtTimezoneValue.lBytes | dtTimeValue.lBytes) == dtForbiddenValue.lBytes);
assert((dtDateValue.lBytes & dtTimeValue.lBytes) == 0);
assert((dtDateValue.lBytes & dtTimezoneValue.lBytes) == 0);
assert((dtTimezoneValue.lBytes & dtTimeValue.lBytes) == 0);

// Reconstruction
dtDateTimeValue.parts.nDate = dtDateValue.parts.nDate;
dtDateTimeValue.SetTimeZone(dtTimezoneValue.GetTimeZone());
dtDateTimeValue.parts.nTime = dtTimeValue.parts.nTime;
cout << "\nReconstructed forbidden value\n";
dtDateTimeValue.WriteInternalFields(cout);
assert(dtDateTimeValue.lBytes == dtForbiddenValue.lBytes);

// Test de la partie annees
cout << "\nTimezone years\n";
dtDateTimeValue.lBytes = 0;
for (i = 0; i < nMaxYear; i++)
{
dtDateTimeValue.fields.nYear = i;
if (i % 1000 == 0)
cout << "." << dtDateTimeValue.fields.nYear;
assert(dtDateTimeValue.fields.nYear == (unsigned int)i);
}
cout << "\n";

// Test de la partie a la frontiere de minutes de timestamp
cout << "\nTimezone minutes\n";
dtDateTimeValue.lBytes = 0;
for (i = 0; i < 60; i++)
{
dtDateTimeValue.SetTimeZoneMinute(i);
cout << "." << dtDateTimeValue.GetTimeZoneMinute();
assert(dtDateTimeValue.GetTimeZoneMinute() == i);
assert(dtDateTimeValue.fields.nTimeZoneSign == 0);
assert(dtDateTimeValue.fields.nTimeZoneHour == 0);
assert(dtDateTimeValue.parts.nDate == 0);
assert(dtDateTimeValue.parts.nTime == 0);
}
cout << "\n";

// Test de la partie des fractions de secondes
cout << "\nTimezone frac seconds\n";
dtDateTimeValue.lBytes = 0;
for (i = 0; i < nMaxFracSeconds; i++)
{
dtDateTimeValue.fields.nFrac = i;
if (i % 100 == 0)
cout << "." << dtDateTimeValue.fields.nFrac;
assert(dtDateTimeValue.fields.nFrac == (unsigned int)i);
}
cout << "\n";
}

void DateTime::WriteInternalFields(ostream& ost) const
{
cout << "dateTimeValue fields\n";
cout << "\tlDateTime bytes: " << lBytes << "\n";
cout << "\tDateTime parts: ((" << parts.nDate << "," << GetTimeZone() << "), " << parts.nTime << ")\n";
cout << "\tDateTime fields: ((" << fields.nYear << "," << fields.nMonth << "," << fields.nDay << " ["
<< fields.nTimeZoneSign << ", " << fields.nTimeZoneHour << ", " << GetTimeZoneMinute() << "]),"
<< fields.nHour << "," << fields.nMinute << "," << fields.nSecond << "," << fields.nFrac << ")\n";
}
Loading

0 comments on commit ea3466b

Please sign in to comment.