Autor Beitrag
baka0815
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 489
Erhaltene Danke: 14

Win 10, Win 8, Debian GNU/Linux
Delphi 10.1 Berlin, Java, C#
BeitragVerfasst: Di 02.09.08 16:32 
Wie in einem anderen Topic angegeben hatte ich ja mit den Delphi-eigenen Methoden zur Datumskonvertierung so meine Probleme und habe mir deswegen einen eigenen, kleinen Parser gebaut, der ein Datum und das Format angegebene bekommt und dann das entsprechende Datum als TDateTime Objekt zurück gibt.

Ich stelle das Ergebnis mal hier rein in der Hoffnung, dass mal wer drüber guckt, ob ich irgendwas übersehen habe bzw. wo noch (größerer oder kleinerer) Optimierungsbedarf besteht:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
function DatumParsen(Datum, Format: string): TDateTime;
var
  I: Integer;
  TagFertig, MonatFertig, JahrFertig: Boolean;
  Tag, Monat: Byte;
  Jahr: Word;
  TagHeute, MonatHeute, JahrHeute: Word;
  Temp: string;
  Von, Laenge: Integer;
begin
  if (Length(Datum) <> Length(Format)) then
    raise EParserError.Create('Der angegebene Text ("' + Datum + '") ist kein Datum im Format "' + Format + '")');

  TagFertig := False; MonatFertig := False; JahrFertig := False;
  Tag := $FF; Monat := $FF; Jahr := $FFFF;

  I := 1;
  while (I <= Length(Format)) do
  begin
    case Format[I] of
      'D'begin
        {
          Wenn das Zeichen "D" erneut vorkommt, ist ein Fehler aufgetreten, der
          Tag wurde bereits geparsed
        }

        if (TagFertig) then
        begin
          raise EParserError.Create('Das angegebene Format ist ungültig: ' + Format);
        end;
        
        Von := I;
        Laenge := 1;
        if (Format[I+1] = 'D'then // DD
        begin
          Laenge := 2;
          Inc(I);
        end;

        try
          Temp := Copy(Datum, Von, Laenge);
          Tag := StrToInt(Temp) and $FF;
        except
          raise EParserError.Create('Der Tag "' + Temp + '" ist keine gültige Zahl!');
        end;

        {
          Prüfen ob der Tag gültig sein kann, weitere Prüfung (z.B. 29.02.)
          weiter unten.
        }

        if (Tag <= 0or (Tag > 31then
          raise EParserError.Create('Der Tag ' + IntToStr(Tag) + ' ist ungültig!');

        TagFertig := True;
      end;
      'M'begin
        {
          Wenn das Zeichen "M" erneut vorkommt, ist ein Fehler aufgetreten, der
          Monat wurde bereits geparsed
        }

        if (MonatFertig) then
        begin
          raise EParserError.Create('Das angegebene Format ist ungültig: ' + Format);
        end;

        Von := I;
        Laenge := 1;
        if (Format[I+1] = 'M'then // MM
        begin
          Laenge := 2;
          Inc(I);
        end;

        try
          Temp := Copy(Datum, Von, Laenge);
          Monat := StrToInt(Temp) and $FF;
        except
          raise EParserError.Create('Der Monat "' + Temp + '" ist keine gültige Zahl!');
        end;

        // Prüfen ob ein gültiger Monat angegeben wurde
        if (Monat <= 0or (Monat > 12then
          raise EParserError.Create('Der Monat ' + IntToStr(Monat) + ' ist ungültig!');        

        MonatFertig := True;
      end;
      'Y'begin
        {
          Wenn das Zeichen "Y" erneut vorkommt, ist ein Fehler aufgetreten, das
          Jahr wurde bereits geparsed
        }

        if (JahrFertig) then
        begin
          raise EParserError.Create('Das angegebene Format ist ungültig: ' + Format);
        end;

        Von := I;
        while (Format[I] = 'Y'do
        begin
          Inc(I);
        end;
        Laenge := I-Von;

        try
          Temp := Copy(Datum, Von, Laenge);
          Jahr := StrToInt(Temp) and $FFFF;
        except
          raise EParserError.Create('Das Jahr "' + Temp + '" ist keine gültige Zahl!');
        end;

        {
          Wenn ein Jahr < 100 angegeben wurde, so muss noch das Jahrhundert
          ergänzt werden (aus 21 wird z.B. 2021).
          Hierzu wird TwoDigitYearCenturyWindow verwendet um zu prüfen welches
          Jahrhunder zu addieren ist.
        }

        if (Jahr < 100then
        begin
          Inc(Jahr, (SysUtils.CurrentYear - TwoDigitYearCenturyWindow) div 100 * 100);
          if (TwoDigitYearCenturyWindow > 0and (Jahr < (SysUtils.CurrentYear - TwoDigitYearCenturyWindow)) then
            Inc(Jahr, 100);
        end;

        JahrFertig := True;
        Dec(I);
      end;
      else
      begin
        // Ungültiger Trenner
        if (Format[I] <> Datum[I]) then
          raise EParserError.Create('Das angegebene Datum ("' + Datum + '") ist nicht im angegebenen Format ("' + Format + '")');
      end;
    end;
    Inc(I);
  end;

  {
    Wenn ein Datumsteil (Jahr, Monat, Tag) im Format nicht angegeben wurde, so
    wird von "Heute" ausgegangen.
  }

  DecodeDate(Now, JahrHeute, MonatHeute, TagHeute);
  if (Tag = $FFthen Tag := TagHeute;
  if (Monat = $FFthen Monat := MonatHeute;
  if (Jahr = $FFFFthen Jahr := JahrHeute;

  {
    Erweiterte Prüfung ob die Anzahl der Tage für den angegebenen Monat auch
    korrekt sind (z.B. 29.02. im Schaltjahr)
  }

  if (Tag > DaysInAMonth(Jahr, Monat)) then
    raise EParserError.Create('Das angegebene Datum ("' + Datum + '") ist ungültig, es gibt nur ' + IntToStr(DaysInAMonth(Jahr, Monat)) + ' Tage im Monat ' + IntToStr(Monat) + ' des Jahres ' + IntToStr(Jahr) + '!');

  // In ein TDateTime Objekt wandeln
  Result := EncodeDate(Jahr, Monat, Tag);
end;


Gültige Aufrufe wären z.B.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
DatumParsen('10.11.2008''DD.MM.YYYY');  // 10.11.2008
DatumParsen('101108''DDMMYY');          // 10.11.2008
DatumParsen('10.11.99''DD.MM.YY');      // 10.11.1999
DatumParsen('3000-02-01''YYYY-MM-DD');  // 01.02.3000


Hier noch der DUnit-Testfall:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
procedure TestAZeile.TestDatumParsen;
var
  Expected, Actual: TDateTime;
begin
  Expected := EncodeDate(20081110);
  Actual := DatumParsen('10.11.2008''DD.MM.YYYY');
  CheckEquals(Expected, Actual, 'Das Datum 10.11.2008 wurde nicht korrekt geparsed!');

  Actual := DatumParsen('10.11.08''DD.MM.YY');
  CheckEquals(Expected, Actual, 'Das Datum 10.11.08 wurde nicht korrekt geparsed!');

  Actual := DatumParsen('101108''DDMMYY');
  CheckEquals(Expected, Actual, 'Das Datum 101108 wurde nicht korrekt geparsed!');

  Actual := DatumParsen('10112008''DDMMYYYY');
  CheckEquals(Expected, Actual, 'Das Datum 10112008 wurde nicht korrekt geparsed!');

  Actual := DatumParsen('10/11/2008''DD/MM/YYYY');
  CheckEquals(Expected, Actual, 'Das Datum 10/11/2008 wurde nicht korrekt geparsed!');

  Expected := EncodeDate(19991110);
  Actual := DatumParsen('10.11.99''DD.MM.YY');
  CheckEquals(Expected, Actual, 'Das Jahr 99 wurde nicht als 1999 aufgefasst.');

  try
    DatumParsen('01/13/2008''DD/MM/YYYY');
    Fail('Das Datum 01/13/2008 ist nicht als falsch erkannt worden!');
  except
  end;

  try
    DatumParsen('80/80/2008''DD/MM/YYYY');
    Fail('Das Datum 80/80/2008 ist nicht als falsch erkannt worden!');
  except
  end;

  try
    DatumParsen('01.01.2008''DD.MM.YYYY');
    Fail('Das Datum 01.01.2008 (beim Format DD/MM/YYYY) ist nicht als falsch erkannt worden!');
  except
  end;

  try
    DatumParsen('31.02.2008''DD.MM.YYYY');
    Fail('Das Datum 31.02.2008 ist nicht als falsch erkannt worden!');
  except
  end;

  Expected := EncodeDate(20080229);
  Actual := DatumParsen('29.02.2008''DD.MM.YYYY');
  CheckEquals(Expected, Actual, 'Das Datum 29.02.2008 wurde nicht korrekt geparsed!');

  try
    DatumParsen('29.02.2006''DD.MM.YYYY');
    Fail('Das Datum 29.02.2006 ist nicht als falsch erkannt worden!');
  except
  end;

  try
    DatumParsen('29.02.1900''DD.MM.YYYY');
    Fail('Das Datum 29.02.1900 ist nicht als falsch erkannt worden!');
  except
  end;

  Expected := EncodeDate(20080229);
  Actual := DatumParsen('2008-02-29''YYYY-MM-DD');
  CheckEquals(Expected, Actual, 'Das Datum 2008-02-29 wurde nicht korrekt geparsed!');

  Actual := DatumParsen('08-02-29''YY-MM-DD');
  CheckEquals(Expected, Actual, 'Das Datum 08-02-29 wurde nicht korrekt geparsed!');

  try
    DatumParsen('1900.02.01''YYYY-MM.DD');
    Fail('Der falsche Trenner wurde nicht erkannt!');
  except
  end;

  try
    DatumParsen('12.12''DD.MM.YYYY');
    Fail('Die unterschiedliche Länge wurde nicht erkannt!');
  except
  end;

  try
    DatumParsen('12.12.1900 1212''DD.MM.YYYY MMDD');
    Fail('Die wiederkehrenden Bezeichner wurden nicht korrekt erkannt!');
  except
  end;

  Expected := Date;
  Actual := DatumParsen('''');
  CheckEquals(Expected, Actual, 'Das Datum <Heute> wurde nicht korrekt erkannt!');

  Expected := EncodeDate(30000201);
  Actual := DatumParsen('3000-02-01''YYYY-MM-DD');
  CheckEquals(Expected, Actual, 'Das Datum 3000-02-01 wurde nicht korrekt erkannt!');

  Expected := EncodeDate(99990201);
  Actual := DatumParsen('9999-02-01''YYYY-MM-DD');
  CheckEquals(Expected, Actual, 'Das Datum 9999-02-01 wurde nicht korrekt erkannt!');

  Expected := EncodeDate(20080131);
  Actual := DatumParsen('31-01-2008''DD-MM-YYYY');
  CheckEquals(Expected, Actual, 'Das Datum 31-01-2008 wurde nicht korrekt erkannt!');
end;


Schon mal im Voraus Danke für Tipps und Kritik!
BenBE
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 8721
Erhaltene Danke: 191

Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
BeitragVerfasst: Mi 03.09.08 01:30 
Sieht schon ganz gut aus, Was noch wünschenswert wäre, wäre das Parsing der Uhrzeit auch gleich mit.

Bug (Index out of Bounds):
Wenn man den Format-Modifier für eine beliebige Angabe einstellig am Ende wählt, tritt bei aktivierten Bound Checks eine Index Out Of Bounds Exception auf. (Gefunden durch pures hingucken ;-))
Betroffen: Zeilen 33, 67 UND 97.

_________________
Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
baka0815 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 489
Erhaltene Danke: 14

Win 10, Win 8, Debian GNU/Linux
Delphi 10.1 Berlin, Java, C#
BeitragVerfasst: Mi 03.09.08 09:18 
user profile iconBenBE hat folgendes geschrieben:
Sieht schon ganz gut aus, Was noch wünschenswert wäre, wäre das Parsing der Uhrzeit auch gleich mit.

Ich parse bewusst nur das Datum, da es bei der Uhrzeit zu Problemen wegen der Format-Angabe führen könnte.
Delphi nimmt ja das Format DD.MM.YYYY HH:MM:SS, wobei die Folge MM 2x vorkommt.
Bei meiner aktuellen Methode wäre das ein Fehler, da er 2x versuchen würde den Monat zu parsen. Dies könnte man lösen indem man sagt, dass Monat = MM und Minuten = mm ist oder so ähnlich (dabei fällt mir auf, dass ich vergessen habe das auf Case-Insensitive zu stellen).
Mein aktuelles Projekt erfordert jedoch nur Datums-Parsen, deswegen weiß ich nicht, ob ich die Zeit noch einbaue.

Achso, Formate wie "DDD" ("Di" bzw. "Tue"), "MMM" ("Jan", "Feb") oder "MMMM" ("Januar", "Februar") habe ich bewusst nicht eingebaut, da ich dann auch die Locale benötigen würde um zu entscheiden was genau erwartet würde und die Routine eh nur innerhalb Deutschlands benötigt wird. Ausserdem sind dann die einzelnen Werte unterschiedlich lang und sowas wie "01Januar2008" bzw. "01März2008" zu parsen ist mit meinem aktuellen Ansatz nicht möglich (unterschiedliche Länge von Format und Datums String).

user profile iconBenBE hat folgendes geschrieben:
Bug (Index out of Bounds):
Wenn man den Format-Modifier für eine beliebige Angabe einstellig am Ende wählt, tritt bei aktivierten Bound Checks eine Index Out Of Bounds Exception auf. (Gefunden durch pures hingucken ;-))
Betroffen: Zeilen 33, 67 UND 97.

Eigentlich würde ich dir Recht geben, tritt jedoch nicht auf, da Format[I+1] bei mir #0 ist, ist also ein nullterminierter String.
Kann das zu Problemen führen bzw. kann man das irgendwie forcieren?

Übergeben habe ich die Daten jetzt sowohl als string-Variable als auch als fester String ('2008-1-1') und beide sind #0-terminiert und laufen somit nicht auf das Problem.
BenBE
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 8721
Erhaltene Danke: 191

Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
BeitragVerfasst: Mi 03.09.08 10:25 
Ältere Delphi-Versionen (D4 und D5) haben bei den Strings eine leicht andere Behandlung gehabt. Abfangen kann man das, indem man If (Length(DeinString) >= Index) and (DeinString[Index] = '?'then als seine Bedingung hernimmt.

_________________
Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
baka0815 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 489
Erhaltene Danke: 14

Win 10, Win 8, Debian GNU/Linux
Delphi 10.1 Berlin, Java, C#
BeitragVerfasst: Mi 03.09.08 10:38 
user profile iconBenBE hat folgendes geschrieben:
Ältere Delphi-Versionen (D4 und D5) haben bei den Strings eine leicht andere Behandlung gehabt.

Gut zu wissen. Kann man das mit neueren Versionen irgendwie forcieren (ich benutze Delphi 2007)?

user profile iconBenBE hat folgendes geschrieben:
Abfangen kann man das, indem man If (Length(DeinString) >= Index) and (DeinString[Index] = '?'then als seine Bedingung hernimmt.

Habe ich bei mir geändert, allerdings stehe ich vor einem Problem, bzw. der Frage ob es überhaupt ein Problem ist. :)

Nun ist es ja möglich als Format YYYY-M-D zu verwenden. "M" und "[delphi]D[/delphi}" stehen hierbei (eigentlich) für Monat und Tag ohne führende 0.
Von mir wird es aktuell jedoch als "genau ein Zeichen" interpretiert, was ich aus folgendem Grund nicht ändern möchte: Wenn das Format "YYYYMD" ist, weiß ich nicht, wie mit dem Wert "2008112" umgegangen werden soll - ist es nun der 12.01.2008 oder der 02.11.2008?

Dann müsste ich hergehen und festlegen, dass einstellige Bezeichner ("M" bzw. "D") nur möglich sind, wenn auch ein Trenner angegeben wird.
Kommentare dazu?
Yogu
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2598
Erhaltene Danke: 156

Ubuntu 13.04, Win 7
C# (VS 2013)
BeitragVerfasst: Mi 03.09.08 13:59 
user profile iconbaka0815 hat folgendes geschrieben:
Delphi nimmt ja das Format DD.MM.YYYY HH:MM:SS, wobei die Folge MM 2x vorkommt.

Genau das Problem hatten die Delphi-Programmierer wohl auch, deshalb haben sie dann für die Minuten ein NN gewählt. ;)