delphi - parseexact - Convertir una cadena a TDateTime en base a un formato arbitrario
iformatprovider datetime c# (8)
Aquí está la función, y sus dos ayudantes, escribí para analizar una cadena usando un formato de fecha y hora exacto . Y dado que Stackoverflow también es un código-wiki: aquí está todo el código:
class function TDateTimeUtils.TryStrToDateExact(const S, DateFormat: string; PivotYear: Integer;
out Value: TDateTime): Boolean;
var
Month, Day, Year: Integer;
Tokens: TStringDynArray;
CurrentToken: string;
i, n: Integer;
Partial: string;
MaxValue: Integer;
nCurrentYear: Integer;
function GetCurrentYear: Word;
var
y, m, d: Word;
begin
DecodeDate(Now, y, m, d);
Result := y;
end;
begin
Result := False;
{
M/dd/yy
Valid pictures codes are
d Day of the month as digits without leading zeros for single-digit days.
dd Day of the month as digits with leading zeros for single-digit days.
ddd Abbreviated day of the week as specified by a LOCALE_SABBREVDAYNAME* value, for example, "Mon" in English (United States).
Windows Vista and later: If a short version of the day of the week is required, your application should use the LOCALE_SSHORTESTDAYNAME* constants.
dddd Day of the week as specified by a LOCALE_SDAYNAME* value.
M Month as digits without leading zeros for single-digit months.
MM Month as digits with leading zeros for single-digit months.
MMM Abbreviated month as specified by a LOCALE_SABBREVMONTHNAME* value, for example, "Nov" in English (United States).
MMMM Month as specified by a LOCALE_SMONTHNAME* value, for example, "November" for English (United States), and "Noviembre" for Spanish (Spain).
y Year represented only by the last digit.
yy Year represented only by the last two digits. A leading zero is added for single-digit years.
yyyy Year represented by a full four or five digits, depending on the calendar used. Thai Buddhist and Korean calendars have five-digit years. The "yyyy" pattern shows five digits for these two calendars, and four digits for all other supported calendars. Calendars that have single-digit or two-digit years, such as for the Japanese Emperor era, are represented differently. A single-digit year is represented with a leading zero, for example, "03". A two-digit year is represented with two digits, for example, "13". No additional leading zeros are displayed.
yyyyy Behaves identically to "yyyy".
g, gg Period/era string formatted as specified by the CAL_SERASTRING value.
The "g" and "gg" format pictures in a date string are ignored if there is no associated era or period string.
PivotYear
The maximum year that a 1 or 2 digit year is assumed to be.
The Microsoft de-factor standard for y2k is 2029. Any value greater
than 29 is assumed to be 1930 or higher.
e.g. 2029:
1930, ..., 2000, 2001,..., 2029
If the PivotYear is between 0 and 99, then PivotYear is assumed to be
a date range in the future. e.g. (assuming this is currently 2010):
Pivot Range
0 1911..2010 (no future years)
1 1912..2011
...
98 2009..2108
99 2010..2099 (no past years)
0 ==> no years in the future
99 ==> no years in the past
}
if Length(S) = 0 then
Exit;
if Length(DateFormat) = 0 then
Exit;
Month := -1;
Day := -1;
Year := -1;
Tokens := TDateTimeUtils.TokenizeFormat(DateFormat);
n := 1; //input string index
for i := Low(Tokens) to High(Tokens) do
begin
CurrentToken := Tokens[i];
if CurrentToken = ''MMMM'' then
begin
//Long month names, we don''t support yet (you''re free to write it)
Exit;
end
else if CurrentToken = ''MMM'' then
begin
//Short month names, we don''t support yet (you''re free to write it)
Exit;
end
else if CurrentToken = ''MM'' then
begin
//Month, with leading zero if needed
if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 1{MinValue}, 12{MaxValue}, {var}Month) then Exit;
end
else if CurrentToken = ''M'' then
begin
//months
if not ReadDigitString(S, n, 1{MinDigits}, 2{MaxDigits}, 1{MinValue}, 12{MaxValue}, {var}Month) then Exit;
end
else if CurrentToken = ''dddd'' then
begin
Exit; //Long day names, we don''t support yet (you''re free to write it)
end
else if CurrentToken = ''ddd'' then
begin
Exit; //Short day names, we don''t support yet (you''re free to write it);
end
else if CurrentToken = ''dd'' then
begin
//If we know what month it is, and even better if we know what year it is, limit the number of valid days to that
if (Month >= 1) and (Month <= 12) then
begin
if Year > 0 then
MaxValue := MonthDays[IsLeapYear(Year), Month]
else
MaxValue := MonthDays[True, Month]; //we don''t know the year, assume it''s a leap year to be more generous
end
else
MaxValue := 31; //we don''t know the month, so assume it''s the largest
if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 1{MinValue}, MaxValue{MaxValue}, {var}Day) then Exit;
end
else if CurrentToken = ''d'' then
begin
//days
//If we know what month it is, and even better if we know what year it is, limit the number of valid days to that
if (Month >= 1) and (Month <= 12) then
begin
if Year > 0 then
MaxValue := MonthDays[IsLeapYear(Year), Month]
else
MaxValue := MonthDays[True, Month]; //we don''t know the year, assume it''s a leap year to be more generous
end
else
MaxValue := 31; //we don''t know the month, so assume it''s the largest
if not ReadDigitString(S, n, 1{MinDigits}, 2{MaxDigits}, 1{MinValue}, MaxValue{MaxValue}, {var}Day) then Exit;
end
else if (CurrentToken = ''yyyy'') or (CurrentToken = ''yyyyy'') then
begin
//Year represented by a full four or five digits, depending on the calendar used.
{
Thai Buddhist and Korean calendars have five-digit years.
The "yyyy" pattern shows five digits for these two calendars,
and four digits for all other supported calendars.
Calendars that have single-digit or two-digit years, such as for
the Japanese Emperor era, are represented differently.
A single-digit year is represented with a leading zero, for
example, "03". A two-digit year is represented with two digits,
for example, "13". No additional leading zeros are displayed.
}
if not ReadDigitString(S, n, 4{MinDigits}, 4{MaxDigits}, 0{MinValue}, 9999{MaxValue}, {var}Year) then Exit;
end
else if CurrentToken = ''yyy'' then
begin
//i''m not sure what this would look like, so i''ll ignore it
Exit;
end
else if CurrentToken = ''yy'' then
begin
//Year represented only by the last two digits. A leading zero is added for single-digit years.
if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 0{MinValue}, 99{MaxValue}, {var}Year) then Exit;
nCurrentYear := GetCurrentYear;
Year := (nCurrentYear div 100 * 100)+Year;
if (PivotYear < 100) and (PivotYear >= 0) then
begin
//assume pivotyear is a delta from this year, not an absolute value
PivotYear := nCurrentYear+PivotYear;
end;
//Check the pivot year value
if Year > PivotYear then
Year := Year - 100;
end
else if CurrentToken = ''y'' then
begin
//Year represented only by the last digit.
if not ReadDigitString(S, n, 1{MinDigits}, 1{MaxDigits}, 0{MinValue}, 9{MaxValue}, {var}Year) then Exit;
nCurrentYear := GetCurrentYear;
Year := (nCurrentYear div 10 * 10)+Year;
if (PivotYear < 100) and (PivotYear >= 0) then
begin
//assume pivotyear is a delta from this year, not an absolute value
PivotYear := nCurrentYear+PivotYear;
end;
//Check the pivot year value
if Year > PivotYear then
Year := Year - 100;
end
else
begin
//The input string should contains CurrentToken starting at n
Partial := Copy(S, n, Length(CurrentToken));
Inc(n, Length(CurrentToken));
if Partial <> CurrentToken then
Exit;
end;
end;
//If there''s still stuff left over in the string, then it''s not valid
if n <> Length(s)+1 then
begin
Result := False;
Exit;
end;
if Day > MonthDays[IsLeapYear(Year), Month] then
begin
Result := False;
Exit;
end;
try
Value := EncodeDate(Year, Month, Day);
except
Result := False;
Exit;
end;
Result := True;
end;
class function TDateTimeUtils.TokenizeFormat(fmt: string): TStringDynArray;
var
i: Integer;
partial: string;
function IsDateFormatPicture(ch: AnsiChar): Boolean;
begin
case ch of
''M'',''d'',''y'': Result := True;
else Result := False;
end;
end;
begin
SetLength(Result, 0);
if Length(fmt) = 0 then
Exit;
//format is only one character long? If so then that''s the tokenized entry
if Length(fmt)=1 then
begin
SetLength(Result, 1);
Result[0] := fmt;
end;
partial := fmt[1];
i := 2;
while i <= Length(fmt) do
begin
//If the characters in partial are a format picture, and the character in fmt is not the same picture code then write partial to result, and reset partial
if IsDateFormatPicture(partial[1]) then
begin
//if the current fmt character is different than the running partial picture
if (partial[1] <> fmt[i]) then
begin
//Move the current partial to the output
//and start a new partial
SetLength(Result, Length(Result)+1);
Result[High(Result)] := partial;
Partial := fmt[i];
end
else
begin
//the current fmt character is more of the same format picture in partial
//Add it to the partial
Partial := Partial + fmt[i];
end;
end
else
begin
//The running partial is not a format picture.
//If the current fmt character is a picture code, then write out the partial and start a new partial
if IsDateFormatPicture(fmt[i]) then
begin
//Move the current partial to the output
//and start a new partial
SetLength(Result, Length(Result)+1);
Result[High(Result)] := partial;
Partial := fmt[i];
end
else
begin
//The current fmt character is another non-picture code. Add it to the running partial
Partial := Partial + fmt[i];
end;
end;
Inc(i);
Continue;
end;
//If we have a running partial, then add it to the output
if partial <> '''' then
begin
SetLength(Result, Length(Result)+1);
Result[High(Result)] := partial;
end;
end;
class function TDateTimeUtils.ReadDigitString(const S: string; var Pos: Integer;
MinDigits, MaxDigits: Integer; MinValue, MaxValue: Integer;
var Number: Integer): Boolean;
var
Digits: Integer;
Value: Integer;
Partial: string;
CandidateNumber: Integer;
CandidateDigits: Integer;
begin
Result := False;
CandidateNumber := -1;
CandidateDigits := 0;
Digits := MinDigits;
while Digits <= MaxDigits do
begin
Partial := Copy(S, Pos, Digits);
if Length(Partial) < Digits then
begin
//we couldn''t get all we wanted. We''re done; use whatever we''ve gotten already
Break;
end;
//Check that it''s still a number
if not TryStrToInt(Partial, Value) then
Break;
//Check that it''s not too big - meaning that getting anymore wouldn''t work
if (Value > MaxValue) then
Break;
if (Value >= MinValue) then
begin
//Hmm, looks good. Keep it as our best possibility
CandidateNumber := Value;
CandidateDigits := Digits;
end;
Inc(Digits); //try to be greedy, grabbing even *MORE* digits
end;
if (CandidateNumber >= 0) or (CandidateDigits > 0) then
begin
Inc(Pos, CandidateDigits);
Number := CandidateNumber;
Result := True;
end;
end;
¿Hay alguna forma en Delphi 5 de convertir una cadena a un TDateTime donde pueda especificar el formato real que se usará?
Estoy trabajando en un procesador de trabajo, que acepta tareas de varias estaciones de trabajo. Las tareas tienen un rango de parámetros, algunos de los cuales son fechas, pero (desafortunadamente, y fuera de mi control) se pasan como cadenas. Como los trabajos pueden provenir de diferentes estaciones de trabajo, el formato de fecha y hora real utilizado para formatear las fechas como una cadena podría (y, por supuesto, la diferencia real).
Buscando en Google, la única solución rápida que encontré fue cambiar la variable ShortDateFormat
y restaurarla a su valor original después. Dado que ShortDateFormat
es una variable global, y estoy trabajando en un entorno de subprocesos, la única forma en que esto funcionaría es sincronizando cada acceso a él, lo cual es completamente inaceptable (y no se puede deshacer).
Podría copiar el código de la biblioteca de la unidad SysUtils
en mis propios métodos, y modificarlos para que funcionen con un formato específico en lugar de las variables globales, pero me pregunto si hay algo más adecuado que no haya visto.
Un cordial saludo, y gracias de antemano,
Willem
ACTUALIZAR
Para decirlo de forma más sucinta:
Necesito algo como StrToDate
(o StrToDateTime
), con la opción agregada de especificar el formato exacto que debería usar para convertir la cadena a un TDateTime.
Creé tal rutina para la unidad de fechas de FreePascal, y debería ser fácil de transportar, si es que se necesita portar.
Código:
(el código es el último (enorme) procedimiento al final del archivo)
documentación:
http://www.freepascal.org/docs-html/rtl/dateutils/scandatetime.html
Tenga en cuenta que no es un inverso completo de formatdatetime y tiene algunas extensiones:
Un inverso de FormatDateTime no es 100% inverso, simplemente porque uno puede poner, por ejemplo, tokens de tiempo dos veces en la cadena de formato, y scandatetime no sabría qué hora elegir.
Cuerdas como hn no se pueden revertir de forma segura. Por ejemplo, 1: 2 (2 minutos después de 1) entrega 12, que se analiza como 12:00 y luego pierde caracteres para la parte "n".
- Los caracteres finales son ignorados.
- no es compatible con los caracteres de formato de Asia oriental, ya que solo son ventanas.
- no hay soporte de MBCS.
Extensiones
- # 9 come espacios en blanco.
- el espacio en blanco al final de un patrón es opcional.
- ? coincide con cualquier char.
- Cita los caracteres anteriores para que coincidan con el personaje.
(Creo que estos comentarios están un poco desactualizados en el sentido de que luego se agregó algo de apoyo asiático, pero no estoy seguro)
Las versiones posteriores de Delphi pueden llevar un argumento TFormatSettings adicional a las funciones de conversión de cadenas. TFormatSettings es una estructura que contiene varias variables globales de formato (ShortDateFormat, LongDateFormat, etc.). Por lo tanto, puede anular ese valor de manera segura para subprocesos e incluso para una sola llamada.
No recuerdo en qué versión de Delphi se introdujo, pero estoy bastante seguro de que fue después de Delphi 5.
Así que sí, por lo que sé, debe sincronizar cada acceso a ShortDateFormat o usar una función diferente.
No estoy seguro de lo que quieres. Ya no uso Delphi 5 pero estoy bastante seguro de que la función StrToDateTime existe en él. Usándolo puedes convertir una cadena a TDateTime con ajustes de formato. Luego, puede convertir dicho TDateTime a cualquier formato usando FormatDateTime, lo que le permite usar cualquier formato de fecha que desee.
Si quieres saber cómo se resolvió esto más adelante en Delphi, puedes echar un vistazo a la fuente de un sysutils.pas un poco más moderno (se parece a Delphi 6) aquí:
http://anygen.googlecome.com/.../SysUtils.pas
Revise las versiones sobrecargadas de StrToDateTime
que toman un parámetro TFormatSettings
.
function StrToDateTime(const S: string;
const FormatSettings: TFormatSettings): TDateTime; overload;
Utilice VarToDateTime en su lugar. Admite muchos más formatos de fecha en la cadena y los convierte automáticamente.
var
DateVal: TDateTime;
begin
DateVal := VarToDateTime(''23 Sep 2010'');
ShowMessage(DateToStr(DateVal));
end;
Veo que estás usando Delphi 5. Algunas versiones de Delphi necesitarán agregar Variantes a la cláusula de usos; La mayoría de las versiones posteriores lo agregan por ti. No recuerdo en qué categoría Delphi 5 cayó.
Utilice la biblioteca RegExpr ( https://github.com/masterandrey/TRegExpr )
var
RE: TRegExpr;
begin
RE := TRegExpr.Create;
try
RE.Expression := ''^(/d/d/d/d)/(/d/d)/(/d/d)T(/d/d):(/d/d):(/d/d)$'';
if RE.Exec( Value ) then
begin
try
Result := EncodeDate( StrToInt( RE.Match[1] ),
StrToInt( RE.Match[2] ),
StrToInt( RE.Match[3] ) ) +
EncodeTime( StrToInt( RE.Match[4] ),
StrToInt( RE.Match[5] ),
StrToInt( RE.Match[6] ),
0 )
except
raise EConvertError.Create( ''Invalid date-time: '' + Value )
end
end
else
raise EConvertError.Create( ''Bad format: '' + Value )
finally
RE.Free
end
end;
Yo iría al revés. Como yo lo veo, tienes dos opciones de las que mencionaste una.
- Ajuste el ShortDateFormat y mantenga todos los accesos sincronizados.
- Si conoce el formato de las cadenas que está recibiendo (de alguna manera, tendrá que hacerlo), simplemente haga algunos malabares de cadenas para obtener primero las cadenas en su formato actual de shortdate . Después de eso, convierta la cadena (malabarizada) a un
TDateTime
.
Me pregunto cómo va a determinar el formato para, por ejemplo, 04/05/2010.
program DateTimeConvert;
{$APPTYPE CONSOLE}
uses
SysUtils;
function GetPart(const part, input, format: string): string;
var
I: Integer;
begin
for I := 1 to Length(format) do
if Uppercase(format[I]) = Uppercase(part) then
Result := Result + input[I];
end;
function GetDay(const input, format: string): string;
begin
Result := GetPart(''d'', input, format);
if Length(Result) = 1 then Result := SysUtils.Format(''0%0:s'', [Result]);
end;
function GetMonth(const input, format: string): string;
begin
Result := GetPart(''m'', input, format);
if Length(Result) = 1 then Result := SysUtils.Format(''0%0:s'', [Result]);
end;
function GetYear(const input, format: string): string;
begin
Result := GetPart(''y'', input, format);
end;
function ConvertToMyLocalSettings(const input, format: string): string;
begin
Result := SysUtils.Format(''%0:s/%1:s/%2:s'', [GetDay(input, format), GetMonth(input, format), GetYear(input, format)]);
end;
begin
Writeln(ConvertToMyLocalSettings(''05/04/2010'', ''dd/mm/yyyy''));
Writeln(ConvertToMyLocalSettings(''05-04-2010'', ''dd-mm-yyyy''));
Writeln(ConvertToMyLocalSettings(''5-4-2010'', ''d-m-yyyy''));
Writeln(ConvertToMyLocalSettings(''4-5-2010'', ''m-d-yyyy''));
Writeln(ConvertToMyLocalSettings(''4-05-2010'', ''M-dd-yyyy''));
Writeln(ConvertToMyLocalSettings(''05/04/2010'', ''dd/MM/yyyy''));
Readln;
end.