{*******************************************************}
{                                                       }
{       Tocsg.DateTime                                  }
{                                                       }
{       Copyright (C) 2022 kkuzil                       }
{                                                       }
{*******************************************************}

unit Tocsg.DateTime;

interface

uses
  System.Generics.Collections, System.Generics.Defaults, System.SysUtils,
  Winapi.Windows;

type
  TDateTimeList = TList<TDateTime>;

  TDateTimeComparer = class(TComparer<TDateTime>)
  public
    function Compare(const Left, Right: TDateTime): Integer; override;
  end;

function ConvStrToDateTime(sDtStr: String): TDateTime; {$IFDEF RELEASE} inline; {$ENDIF}
function ConvSecToProgTime(dwSec: DWORD): String; inline;
function ConvSecBetweenToProgTime(dtNow, dtThen: TDateTime): String;

function GetBootTime: TDateTime;
function GetLocalIncUtcMin: Integer; // 로컬 타임존 UTC+분

function ConvFileTimeToDateTime(const aFileTime: TFileTime): TDateTime;
function ConvFileTimeToDateTime_Local(const aFileTime: TFileTime): TDateTime;
function ConvSystemTimeToDateTime(const st: TSystemTime): TDateTime;
function ConvSystemTimeToDateTime_Local(const st: TSystemTime): TDateTime;
function ConvDateTimeToSystemTime(dt: TDateTime): TSystemTime;
function ConvDateTimeToSystemTime_Local(dt: TDateTime): TSystemTime;
function ConvDateTimeToFileTime(dt: TDateTime): TFileTime;
function ConvUtcToLocal(dt: TDateTime): TDateTime;
function ConvLocalToUtc(dt: TDateTime): TDateTime;

function ConvTimeToDateTime(t: Integer): TDateTime;
function ConvTimestampToDateTime(llTime: LONGLONG): TDateTime;
function ConvChromeTimestampToDateTime(llTime: LONGLONG): TDateTime;
function ConvTimestampToFileTime_Local(llTime: LONGLONG): TFileTime;
function ConvTimestampToFileTime(llTime: LONGLONG): TFileTIme;
function ConvJavaToDelphiDateTime(const llVal: LONGLONG; bUtc0: Boolean = false): TDateTime;

function ConvStrDtToDateTime(const sDateTime: String): TDateTime;
function UnixTimeToDateTime(llDT: LONGLONG): TDateTime;

function AppendTime(dt: TDateTime; wHour, wMin, wSec, wMSec: WORD): TDateTime;

function RawStrToDateTime(const RawStr: string): TDateTime;

function JavaDtToDelphiDt(const dt: int64): TDateTime;
function DelphiDtToJavaDt(const dt: TDateTime): int64;

implementation

uses
  System.DateUtils, Tocsg.Exception;

{ TDateTimeComparer }

function TDateTimeComparer.Compare(const Left, Right: TDateTime): Integer;
begin
  Result := CompareDateTime(Right, Left);
end;

{ Other }

function ConvStrToDateTime(sDtStr: String): TDateTime;
var
  wLen, wYear, wMonth, wDay, wHour, wMin, wSec: WORD;
begin
  Result := 0;

  sDtStr := StringReplace(sDtStr, '-', '', [rfReplaceAll]);
  sDtStr := StringReplace(sDtStr, ':', '', [rfReplaceAll]);
  sDtStr := StringReplace(sDtStr, ' ', '', [rfReplaceAll]);

  wLen := sDtStr.Length;
  if wLen >= 8 then
  begin
    wYear := StrToIntDef(Copy(sDtStr, 1, 4), 0);
    wMonth := StrToIntDef(Copy(sDtStr, 5, 2), 0);
    wDay := StrToIntDef(Copy(sDtStr, 7, 2), 0);
    if wLen >= 14 then
    begin
      wHour := StrToIntDef(Copy(sDtStr, 9, 2), 0);
      wMin := StrToIntDef(Copy(sDtStr, 11, 2), 0);
      wSec := StrToIntDef(Copy(sDtStr, 13, 2), 0);
    end else begin
      wHour := 0;
      wMin := 0;
      wSec := 0;
    end;

    try
      Result := EncodeDateTime(wYear, wMonth, wDay, wHour, wMin, wSec, 0);
    except
      exit;
    end;
  end;
end;

function ConvSecToProgTime(dwSec: DWORD): String;
var
  dwMin,
  dwHour, dwDay: DWORD;
begin
  dwMin  := (dwSec div 60) mod 60;
  dwHour := (dwSec div 60 div 60) mod 24;
  dwDay  := dwSec div 60 div 60 div 24;
  dwSec  := dwSec mod 60;
  if dwDay > 0 then
  begin
    if (dwHour + dwMin + dwSec) = 0 then
      Result := Format('%dDay', [dwDay])
    else
      Result := Format('%dDay, %.2d:%.2d:%.2d', [dwDay, dwHour, dwMin, dwSec]);
  end else
    Result := Format('%.2d:%.2d:%.2d', [dwHour, dwMin, dwSec]);
end;

function ConvSecBetweenToProgTime(dtNow, dtThen: TDateTime): String;
begin
  Result := ConvSecToProgTime(SecondsBetween(dtNow, dtThen));
end;

function GetBootTime: TDateTime;
begin
  Result := IncMilliSecond(now, LONGLONG(-1) * GetTickCount);
end;

function GetLocalIncUtcMin: Integer;
begin
  Result := (TTimeZone.Local.UtcOffset.Hours * 60) + TTimeZone.Local.UtcOffset.Minutes;
end;

function ConvFileTimeToDateTime(const aFileTime: TFileTime): TDateTime;
var
  nDosTime: Integer;
begin
  Result := 0;
  try
    if WinApi.Windows.FileTimeToDosDateTime(aFileTime, LongRec(nDosTime).Hi,
        LongRec(nDosTime).Lo) then
      try
        Result := FileDateToDateTime(nDosTime);
      except
        On EConvertError do
          exit;
      end;
  except
    exit;
  end;
end;

function ConvFileTimeToDateTime_Local(const aFileTime: TFileTime): TDateTime;
var
  lcFT : TFileTime;
  lc: Integer;
begin
  Result := 0;
  if WinApi.Windows.FileTimeToLocalFileTime(aFileTime, lcFT) then
  begin
    if WinApi.Windows.FileTimeToDosDateTime(lcFT, LongRec(lc).Hi, LongRec(lc).Lo) then
      try
        Result := FileDateToDateTime(lc);
      except
        On EConvertError do
          exit;
      end;
  end;
end;

function ConvSystemTimeToDateTime(const st: TSystemTime): TDateTime;
var
  ft: TFileTime;
begin
  Result := 0;

  if SystemTimeToFileTime(st, ft) then
    Result := ConvFileTimeToDateTime(ft);
end;

function ConvSystemTimeToDateTime_Local(const st: TSystemTime): TDateTime;
var
  ft: TFileTime;
begin
  Result := 0;

  if SystemTimeToFileTime(st, ft) then
    Result := ConvFileTimeToDateTime_Local(ft);
end;

function ConvDateTimeToSystemTime(dt: TDateTime): TSystemTime;
begin
  try
    if not FileTimeToSystemTime(ConvDateTimeToFileTime(dt), Result) then
      ZeroMemory(@Result, SizeOf(Result));
  except
    on E: Exception do
    begin
      ETgException.TraceException(E, 'Fail .. ConvDateTimeToSystemTime()');
      ZeroMemory(@Result, SizeOf(Result));
    end;
  end;
end;

function ConvDateTimeToSystemTime_Local(dt: TDateTime): TSystemTime;
var
  lcFT: TFileTime;
begin
  ZeroMemory(@Result, SizeOf(Result));
  try
    if WinApi.Windows.FileTimeToLocalFileTime(ConvDateTimeToFileTime(dt), lcFT) then
      FileTimeToSystemTime(lcFT, Result);
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. ConvDateTimeToSystemTime_Local()');
  end;
end;

function ConvDateTimeToFileTime(dt: TDateTime): TFileTime;
var
  LocalFileTime, Ft: TFileTime;
  SystemTime: TSystemTime;
begin
  Result.dwLowDateTime  := 0;
  Result.dwHighDateTime := 0;
  try
    DateTimeToSystemTime(dt, SystemTime);
    SystemTimeToFileTime(SystemTime, LocalFileTime);
    LocalFileTimeToFileTime(LocalFileTime, Ft);
    Result := Ft;
  except
    ZeroMemory(@Result, SizeOf(Result));
  end;
end;

function ConvUtcToLocal(dt: TDateTime): TDateTime;
var
  tzi: TTimeZoneInformation;
  lt, ut: TSystemTime;
begin
  Result := 0;
  try
    GetTimeZoneInformation(tzi);
    DateTimeToSystemTime(dt, ut);
    SystemTimeToTzSpecificLocalTime(@tzi, ut, lt);
    Result := SystemTimeToDateTime(lt);
  except

  end;
end;

function ConvLocalToUtc(dt: TDateTime): TDateTime;
begin
  Result := 0;
  try
    Result := TTimeZone.Local.ToUniversalTime(Now);
  except

  end;
end;

function ConvTimeToDateTime(t: Integer): TDateTime;
var
  sysTime, sysTime1 : TSystemTime;
begin
  DateTimeToSystemTime(EncodeDate(1970, 1, 1) + (t / 86400), sysTime);
  SystemTimeToTzSpecificLocalTime(nil, sysTime, sysTime1);
  Result := SystemTimeToDateTime(sysTime1);
end;

function ConvTimestampToDateTime(llTime: LONGLONG): TDateTime;
var
  nTime: Integer;
  ft: TFileTime;
begin
  Result := 0;
// 이 값이 들어오는 경우가 있는데..
// 이 경우는 FileDateToDateTime() 여기서 예외가 발생해서 미리 체크함 20_0522 09:26:30 sunk
  if llTime <> 116444736000000000 then
  begin
    CopyMemory(@ft, @llTime, SizeOf(LONGLONG));
    try
      FileTimeToDosDateTime(ft, LongRec(nTime).Hi, LongRec(nTime).Lo);
      if nTime > 0 then
        Result := FileDateToDateTime(nTime);
    except
//      On EConvertError do
//        exit;
    end;
  end;
end;

function ConvChromeTimestampToDateTime(llTime: LONGLONG): TDateTime;
begin
{
u :  Unix       timestamp    eg: 1378615325
j :  JavaScript timestamp    eg: 1378615325177
c :  Chrome     timestamp    eg: 13902597987770000
w :  Windows    timestamp    eg: 139025979877700000

u =  (j / 1000)
u =  (c - 116444736000000)   / 10000000
u =  (w - 1164447360000000)  / 100000000

j =  (u * 1000)
j =  (c - 116444736000000)   / 10000
j =  (w - 1164447360000000)  / 100000

c =  (u * 10000000)          + 116444736000000
c =  (j * 10000)             + 116444736000000
c =  (w / 10)

w =  (u * 100000000)         + 1164447360000000
w =  (j * 100000)            + 1164447360000000
w =  (c * 10)
}
  if llTime > 116444736000000 then
    Result := ConvTimestampToDateTime(llTime * 10)
  else
    Result := 0;
end;

function ConvTimestampToFileTime_Local(llTime: LONGLONG): TFileTime;
var
  ft,
  localft: TFileTime;
begin
  CopyMemory(@ft, @llTime, SizeOf(LONGLONG));

  try
    FileTimeToLocalFileTime(ft, localft); // 생성일 기준
    Result := localft;
  except
    ZeroMemory(@Result, SizeOf(Result));
  end;
end;

function ConvTimestampToFileTime(llTime: LONGLONG): TFileTIme;
var
  ft: TFileTime;
begin
  CopyMemory(@ft, @llTime, SizeOf(LONGLONG));
  Result := ft;
end;

// superobject.pas에 있는거 가져옴
function ConvJavaToDelphiDateTime(const llVal: LONGLONG; bUtc0: Boolean = false): TDateTime;
var
  t: TSystemTime;
begin
  DateTimeToSystemTime(25569 + (llVal / 86400000), t);
  if not bUtc0 then
    SystemTimeToTzSpecificLocalTime(nil, t, t);
  Result := SystemTimeToDateTime(t);
end;

function ConvStrDtToDateTime(const sDateTime: String): TDateTime;
var
  wLen, wYear, wMonth, wDay, wHour, wMin, wSec: WORD;
begin
  Result := 0;

  wLen := Length(sDateTime);
  if wLen >= 8 then
  begin
    wYear := StrToIntDef(Copy(sDateTime, 1, 4), 0);
    wMonth := StrToIntDef(Copy(sDateTime, 5, 2), 0);
    wDay := StrToIntDef(Copy(sDateTime, 7, 2), 0);
    if wLen >= 14 then
    begin
      wHour := StrToIntDef(Copy(sDateTime, 9, 2), 0);
      wMin := StrToIntDef(Copy(sDateTime, 11, 2), 0);
      wSec := StrToIntDef(Copy(sDateTime, 13, 2), 0);
    end else begin
      wHour := 0;
      wMin := 0;
      wSec := 0;
    end;

    try
      Result := EncodeDateTime(wYear, wMonth, wDay, wHour, wMin, wSec, 0);
    except
      Result := 0;
    end;
  end;
end;

function UnixTimeToDateTime(llDT: LONGLONG): TDateTime;
begin
  Result := (llDT / 86400) + 25569;
end;

function AppendTime(dt: TDateTime; wHour, wMin, wSec, wMSec: WORD): TDateTime;
var
  wYear, wMonth, wDay: WORD;
begin
  try
    DecodeDate(dt, wYear, wMonth, wDay);
    Result := EncodeDateTime(wYear, wMonth, wDay, wHour, wMin, wSec, wMSec);
  except
    Result := dt;
  end;
end;

function RawStrToDateTime(const RawStr: string): TDateTime;
var
  v: Int64;
begin
  v := StrToInt64(RawStr);
  Result := PDouble(@v)^;
end;

// superobject.pas에서 가져옴
function DayLightCompareDate(const date: PSystemTime;
  const compareDate: PSystemTime): Integer;
var
  limit_day, dayinsecs, weekofmonth: Integer;
  First: Word;
begin
  if (date^.wMonth < compareDate^.wMonth) then
  begin
    Result := -1; (* We are in a month before the date limit. *)
    Exit;
  end;

  if (date^.wMonth > compareDate^.wMonth) then
  begin
    Result := 1; (* We are in a month after the date limit. *)
    Exit;
  end;

  (* if year is 0 then date is in day-of-week format, otherwise
   * it's absolute date.
   *)
  if (compareDate^.wYear = 0) then
  begin
    (* compareDate.wDay is interpreted as number of the week in the month
     * 5 means: the last week in the month *)
    weekofmonth := compareDate^.wDay;
    (* calculate the day of the first DayOfWeek in the month *)
    First := (6 + compareDate^.wDayOfWeek - date^.wDayOfWeek + date^.wDay) mod 7 + 1;
    limit_day := First + 7 * (weekofmonth - 1);
    (* check needed for the 5th weekday of the month *)
    if (limit_day > MonthDays[(date^.wMonth=2) and IsLeapYear(date^.wYear)][date^.wMonth]) then
      dec(limit_day, 7);
  end
  else
    limit_day := compareDate^.wDay;

  (* convert to seconds *)
  limit_day := ((limit_day * 24  + compareDate^.wHour) * 60 + compareDate^.wMinute ) * 60;
  dayinsecs := ((date^.wDay * 24  + date^.wHour) * 60 + date^.wMinute ) * 60 + date^.wSecond;
  (* and compare *)

  if dayinsecs < limit_day then
    Result :=  -1 else
    if dayinsecs > limit_day then
      Result :=  1 else
      Result :=  0; (* date is equal to the date limit. *)
end;

function CompTimeZoneID(const pTZinfo: PTimeZoneInformation;
  lpFileTime: PFileTime; islocal: Boolean): LongWord;
var
  ret: Integer;
  beforeStandardDate, afterDaylightDate: Boolean;
  llTime: Int64;
  SysTime: TSystemTime;
  ftTemp: TFileTime;
begin
  llTime := 0;

  if (pTZinfo^.DaylightDate.wMonth <> 0) then
  begin
    (* if year is 0 then date is in day-of-week format, otherwise
     * it's absolute date.
     *)
    if ((pTZinfo^.StandardDate.wMonth = 0) or
        ((pTZinfo^.StandardDate.wYear = 0) and
        ((pTZinfo^.StandardDate.wDay < 1) or
        (pTZinfo^.StandardDate.wDay > 5) or
        (pTZinfo^.DaylightDate.wDay < 1) or
        (pTZinfo^.DaylightDate.wDay > 5)))) then
    begin
      SetLastError(ERROR_INVALID_PARAMETER);
      Result := TIME_ZONE_ID_INVALID;
      Exit;
    end;

    if (not islocal) then
    begin
      llTime := PInt64(lpFileTime)^;
      dec(llTime, Int64(pTZinfo^.Bias + pTZinfo^.DaylightBias) * 600000000);
      PInt64(@ftTemp)^ := llTime;
      lpFileTime := @ftTemp;
    end;

    FileTimeToSystemTime(lpFileTime^, SysTime);

    (* check for daylight savings *)
    ret := DayLightCompareDate(@SysTime, @pTZinfo^.StandardDate);
    if (ret = -2) then
    begin
      Result := TIME_ZONE_ID_INVALID;
      Exit;
    end;

    beforeStandardDate := ret < 0;

    if (not islocal) then
    begin
      dec(llTime, Int64(pTZinfo^.StandardBias - pTZinfo^.DaylightBias) * 600000000);
      PInt64(@ftTemp)^ := llTime;
      FileTimeToSystemTime(lpFileTime^, SysTime);
    end;

    ret := DayLightCompareDate(@SysTime, @pTZinfo^.DaylightDate);
    if (ret = -2) then
    begin
      Result := TIME_ZONE_ID_INVALID;
      Exit;
    end;

    afterDaylightDate := ret >= 0;

    Result := TIME_ZONE_ID_STANDARD;
    if( pTZinfo^.DaylightDate.wMonth < pTZinfo^.StandardDate.wMonth ) then
    begin
      (* Northern hemisphere *)
      if( beforeStandardDate and afterDaylightDate) then
        Result := TIME_ZONE_ID_DAYLIGHT;
    end else    (* Down south *)
      if( beforeStandardDate or afterDaylightDate) then
        Result := TIME_ZONE_ID_DAYLIGHT;
  end else
    (* No transition date *)
    Result := TIME_ZONE_ID_UNKNOWN;
end;

function GetTimezoneBias(const pTZinfo: PTimeZoneInformation;
  lpFileTime: PFileTime; islocal: Boolean; pBias: PLongint): Boolean;
var
  bias: LongInt;
  tzid: LongWord;
begin
  bias := pTZinfo^.Bias;
  tzid := CompTimeZoneID(pTZinfo, lpFileTime, islocal);

  if( tzid = TIME_ZONE_ID_INVALID) then
  begin
    Result := False;
    Exit;
  end;
  if (tzid = TIME_ZONE_ID_DAYLIGHT) then
    inc(bias, pTZinfo^.DaylightBias)
  else if (tzid = TIME_ZONE_ID_STANDARD) then
    inc(bias, pTZinfo^.StandardBias);
  pBias^ := bias;
  Result := True;
end;

function TzSpecificLocalTimeToSystemTime(
    const lpTimeZoneInformation: PTimeZoneInformation;
    const lpLocalTime: PSystemTime; lpUniversalTime: PSystemTime): BOOL;
var
  ft: TFileTime;
  lBias: LongInt;
  t: Int64;
  tzinfo: TTimeZoneInformation;
begin
  if (lpTimeZoneInformation <> nil) then
    tzinfo := lpTimeZoneInformation^
  else
    if (GetTimeZoneInformation(tzinfo) = TIME_ZONE_ID_INVALID) then
    begin
      Result := False;
      Exit;
    end;

  if (not SystemTimeToFileTime(lpLocalTime^, ft)) then
  begin
    Result := False;
    Exit;
  end;
  t := PInt64(@ft)^;
  if (not GetTimezoneBias(@tzinfo, @ft, True, @lBias)) then
  begin
    Result := False;
    Exit;
  end;
  (* convert minutes to 100-nanoseconds-ticks *)
  inc(t, Int64(lBias) * 600000000);
  PInt64(@ft)^ := t;
  Result := FileTimeToSystemTime(ft, lpUniversalTime^);
end;

function SystemTimeToTzSpecificLocalTime(
  lpTimeZoneInformation: PTimeZoneInformation;
  lpUniversalTime, lpLocalTime: PSystemTime): BOOL;
var
  ft: TFileTime;
  lBias: LongInt;
  llTime: Int64;
  tzinfo: TTimeZoneInformation;
begin
  if (lpTimeZoneInformation <> nil) then
    tzinfo := lpTimeZoneInformation^ else
    if (GetTimeZoneInformation(tzinfo) = TIME_ZONE_ID_INVALID) then
    begin
      Result := False;
      Exit;
    end;

  if (not SystemTimeToFileTime(lpUniversalTime^, ft)) then
  begin
    Result := False;
    Exit;
  end;
  llTime := PInt64(@ft)^;
  if (not GetTimezoneBias(@tzinfo, @ft, False, @lBias)) then
  begin
    Result := False;
    Exit;
  end;
  (* convert minutes to 100-nanoseconds-ticks *)
  dec(llTime, Int64(lBias) * 600000000);
  PInt64(@ft)^ := llTime;
  Result := FileTimeToSystemTime(ft, lpLocalTime^);
end;

function JavaDtToDelphiDt(const dt: int64): TDateTime;
var
  t: TSystemTime;
begin
  DateTimeToSystemTime(25569 + (dt / 86400000), t);
  SystemTimeToTzSpecificLocalTime(nil, @t, @t);
  Result := SystemTimeToDateTime(t);
end;

function DelphiDtToJavaDt(const dt: TDateTime): int64;
var
  t: TSystemTime;
begin
  DateTimeToSystemTime(dt, t);
  TzSpecificLocalTimeToSystemTime(nil, @t, @t);
  Result := Round((SystemTimeToDateTime(t) - 25569) * 86400000)
end;

end.
