Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Cross-Platform-Entwicklung (https://www.delphipraxis.net/91-cross-platform-entwicklung/)
-   -   Delphi Lokale Windows Zeitzone in IANA format ermitteln (https://www.delphipraxis.net/203331-lokale-windows-zeitzone-iana-format-ermitteln.html)

QuickAndDirty 7. Feb 2020 08:52


Lokale Windows Zeitzone in IANA format ermitteln
 
Liste der Anhänge anzeigen (Anzahl: 2)
Da plattformübergreifend meist mit IANA Zeitzonen gearbeitet wird und ich TZDB.pas von der IANA nutze, ist es notwendig dass ich Windowszeitzonen in IANA-Format ermitteln kann.

Ich will einfach nur zeigen wie ich das gemacht habe. Falls jemand Verbessungen hat nur her damit.

Wie sich herausgestellt hat liefert
TTimeZone.Local.ID und getTimeZoneInfo(TZI) immer nur die lokalisierte Bezeichnung
'Westwuropäische Zeit'.
Das hilft natürlich nicht, wenn man eine IANA kompatible Bezeichnung haben möchte.

Also ermittle ich die Zeitzone aus der Registry :(
Delphi-Quellcode:
Function GetCurrentTimeZoneID:String;
Begin
{$IFDEF WIN32}
  var Reg := TRegistry.Create(KEY_READ);
  try
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    if Reg.OpenKeyReadOnly('SYSTEM\CurrentControlSet\Control\TimeZoneInformation') then
      result := GetSaveTimeZoneID( reg.GetDataAsString('TimeZoneKeyName') );
  finally
    Reg.Free;
  end;
{$ELSE}}
  Result := TTimeZone.Local.ID;
{$ENDIF}
End;
GetSaveTimeZoneID sucht nach der passenden IANA Zeitzone und liefert einen Leerstring zurrück wenn nichts passendes gefunden werden kann.
Man Braucht zwingend TZDB.PAS und TZDB.INC von der IANA damit das ganze funktioniert!
Für das eigene Projekt muss man die {$INCLUDE c:\... } Pfade anpassen. Es ist kein installierbares Package!

Delphi-Quellcode:
function GetSaveTimeZoneID(aTimeZoneID:String): String;
begin
  var hit:String := '';

  for var i:integer := Low(CZones) to High(CZones) do
  Begin
    if SameText(CZones[i].FName, ATimeZoneID) then
    begin
      hit := CZones[i].FName;
      break;
    end;
  End;

  if hit = '' then
  Begin
    for var i:integer := Low(CAliases) to High(CAliases) do
    Begin
      if SameText(CAliases[i].FName, ATimeZoneID) then
      begin
        hit := CAliases[i].FAliasTo^.FName ;
        break;
      end;
    End;
  End;

  if hit = '' then
  Begin
    for var i:integer := Low(CWindowsTimeZones) to High(CWindowsTimeZones) do
    Begin
      if SameText(CWindowsTimeZones[i].Windows_Name, ATimeZoneID) or
         SameText(CWindowsTimeZones[i].IANA_Name, ATimeZoneID) or
         SameText(CWindowsTimeZones[i].ISO3166_Name, ATimeZoneID) then
      begin
        hit := CZones[CWindowsTimeZones[i].CZoneIndex ].FName;
        break;
      end;
    End;
  End;

  Result := hit;
end;
Die "WindowsTimezones.inc" Datei generiere ich aus der WindowsTimezones.XML Datei des CDLR Projekts von Unicode.org .
Ich weiß das der Code dafür nicht so toll aussieht, aber ich wollte ihn an sich auch nur alle 6 Monate ausführen....er muss also auch nicht effizient sein.
Weil in TZDB.PAS die Typen alle Privat deklariert sind und keine TZDBTypes.Pas Datei existiert ...muss man die Typen und die Include-Datei TZDB.INC überall mit rein kopieren...
Wenn ihr euch die WindowsTimezones.inc nicht selbst generieren wollt braucht man den Codegenerator auch nicht.
Ich habe eine WindowsTimezones.inc angehängt.
Delphi-Quellcode:
implementation

{$R *.fmx}
uses TZDB;

type
  { Day type. Specifies the "relative" day in a month }
  TDayType = (dtFixed, dtLastOfMonth, dtNthOfMonth, dtPredOfMonth);

  { Specifies the mode in which a time value is specified }
  TTimeMode = (trLocal, trStandard, trUniversal);

  { Stores the information about the relative days }
  TRelativeDay = record
    case FDayType: TDayType of
      dtFixed:
        (FFixedDay: Word);
      dtLastOfMonth:
        (FLastDayOfWeek: Word);
      dtNthOfMonth:
        (FNthDayOfWeek: Word; FNthDayIndex: Word);
      dtPredOfMonth:
        (FPredDayOfWeek: Word; FPredDayIndex: Word);
  end;

  { Pointer to a relative day }
  PRelativeDay = ^TRelativeDay;

  { Defines a rule used for DST changes }
  TRule = record
    FInMonth: Word; { The month (1 - 12) when DST change occurs }
    FOnDay: PRelativeDay; { Pointer to a TRelativeDay value }
    FAt: Int64; { Time, in seconds }
    FAtMode: TTimeMode; { Time relation mode }
    FOffset: Int64; { Offset from GMT in seconds }
    FFmtPart: string;
    { A symbolic string used later when building short TZ names }
  end;

  { Pointer to a rule }
  PRule = ^TRule;

  { Defines a rule that also has a validity date defined }
  TYearBoundRule = record
    FStart: Word; { The year in which the rule starts to apply }
    FEnd: Word; { The year in which the rule ends to apply }
    FRule: PRule; { A pointer to the actual rule }
  end;

  { Pointer to a year-bound rule entry }
  PYearBoundRule = ^TYearBoundRule;

  { Defines a rule family. If fact it is a set of rules combined under the same ID }
  TRuleFamily = record
    FCount: Integer; { Count of rule in the current family }
    FFirstRule: PYearBoundRule;
    { Pointer to the first rule in a static array defined previously }
  end;

  { A pointer to a rule family }
  PRuleFamily = ^TRuleFamily;

  { A period of some years (for a zone) that defines specific DST rules and offsets }
  TPeriod = record
    FOffset: Integer; { GMT offset in seconds for this period of time }
    FRuleFamily: PRuleFamily;
    { Pointer to the family if rules that apply to this period }
    FFmtStr: string;
    { Format string that will get translated in certain conditions }
    FUntilYear, FUntilMonth: Word; { Period is valid until this Year/Month }
    FUntilDay: PRelativeDay; { Period is valid until this Day in Year/Month }
    FUntilTime: Int64;
    FUntilTimeMode: TTimeMode; { Time relation mode }
    { Period is valid until this time of day Day in Year/Month. In seconds }
  end;

  { Pointer to a TPeriod }
  PPeriod = ^TPeriod;

  { Defines a time-zone. }
  TZone = record
    FName: string; { Zone name (aka Europe/Romania, Europe/London etc) }
    FCount: Integer; { Count of periods defined by this zone }
    FFirstPeriod: PPeriod; { Pointer to the first TPeriod for this zone }
  end;

  { Pointer to a zone object }
  PZone = ^TZone;

  { Alias to a zone }
  TZoneAlias = record
    FName: string; { Name of the zone to alias }
    FAliasTo: PZone; { Pointer to aliased zone }
  end;
{$INCLUDE 'TZDB.inc'}

  type
  TWindowsTimeZone = record
    Index: Integer;
    Windows_Name: String;
    IANA_Name: String;
    ISO3166_Name: String;
    CZoneIndex:Integer;
  end;



procedure TIncFileGenForm.StartBTNClick(Sender: TObject);

  Function IndexOfZoneName(AZoneName:String):Integer;
  Begin
    Result := -1;
    for var i:integer := 0 to length(CZones)-1 do
    Begin
      if SameText(CZones[i].FName,AZoneName) then
      Begin
        Result := i;
        break;
      End;
    End;
  End;

  Function IndexOfZoneAlias(AZoneName:String):Integer;
  Begin
    Result := -1;
    for var i:Integer := 0 to length(CAliases)-1 do
    Begin
      if SameText(CAliases[i].FName,AZoneName) then
      Begin
        Result := i;
        break;
      End;
    End;
  End;

  Function AliasIndexToCZoneIndex(aAliasIndex:Integer):Integer;
  Begin
    Result := IndexOfZoneName(CAliases[aAliasIndex].FAliasTo^.FName);
  End;

  Function IndexOfWindowsTimeZone(aWindowsTimeZones:TList<TWindowsTimeZone>; aIANA_Name,aISO3166_Name ,aWindows_Name:String):integer;
  var Currentelement:TWindowsTimeZone;
  Begin
    Result := -1;
    for var i:integer := 0 to aWindowsTimeZones.Count-1 do
    Begin
      Currentelement := aWindowsTimeZones[i];
      if CurrentElement.IANA_Name.Equals(aIANA_Name) and
         CurrentElement.Windows_Name.Equals(aWindows_Name) and
         CurrentElement.ISO3166_Name.Equals(aISO3166_Name) then
      Begin
        Result := i;
        break;
      End;
    End;
  End;

  Procedure EnterEntry(aWindowsTimeZones:TList<TWindowsTimeZone>; aIANA_Name, aISO3166_Name, aWindows_Name:String; CZoneIndex:Integer);
  var CurrentWindowsTimeZone:TWindowsTimeZone;
  Begin
    if IndexOfWindowsTimeZone(aWindowsTimeZones, aIANA_Name,aISO3166_Name,aWindows_Name) = -1 then   // Keine Doppelten
    Begin
      CurrentWindowsTimeZone.Index := aWindowsTimeZones.Count;
      CurrentWindowsTimeZone.IANA_Name := aIANA_Name;
      CurrentWindowsTimeZone.ISO3166_Name := aISO3166_Name;
      CurrentWindowsTimeZone.Windows_Name := aWindows_Name;
      CurrentWindowsTimeZone.CZoneIndex := CZoneIndex;
      aWindowsTimeZones.Add(CurrentWindowsTimeZone);
    End;
  End;

  procedure ReadDocument(aWindowsTimeZones:TList<TWindowsTimeZone>);
  var
    LDocument: IXMLDocument;
    LNodeElement,root ,LSuplementData, LWindowsZones, LMapTimeZones,LCurrentTimeZone,NodeCData, NodeText: IXMLNode;
    LTimeZoneList: IXMLNodeList;
    FirstIndex:Integer;
    IANA_NameList,IANA_Name, ISO3166_Name, Windows_Name:String;
    IANA_NameArr:TArray<String>;
  Begin
    aWindowsTimeZones.Clear;
    LDocument := TXMLDocument.Create(
                                     TPath.Combine( TPath.GetDirectoryName(paramstr(0)), 'windowsZones.xml')
                                     ) as IXMLDocument;
    root := LDocument.DocumentElement;
    LSuplementData := LDocument.ChildNodes[root.NodeName];
    LWindowsZones := LSuplementData.ChildNodes['windowsZones'];
    LMapTimeZones := LWindowsZones.ChildNodes['mapTimezones'];
    LTimeZoneList := LMapTimeZones.ChildNodes;

    for var i:Integer := 0 to LTimeZoneList.Count-1 do
    Begin
      LCurrentTimeZone := LTimeZoneList[i];
      If LCurrentTimeZone.NodeName = 'mapZone' then
      Begin
        IANA_NameList := LCurrentTimeZone.Attributes['type'];
        IANA_NameArr := IANA_NameList.Split([' ']);
        ISO3166_Name := LCurrentTimeZone.Attributes['territory'];
        Windows_Name := LCurrentTimeZone.Attributes['other'];
        for var j:integer := 0 to Length( IANA_NameArr )-1 do
        Begin
          IANA_Name := IANA_NameArr[j];
          FirstIndex := IndexOfZoneName( IANA_Name );
           If FirstIndex >= 0 then
          Begin
            EnterEntry(aWindowsTimeZones, IANA_Name, ISO3166_Name, Windows_Name, FirstIndex );
          end// If FirstIndex >= 0 then
          else
          Begin
            FirstIndex := IndexOfZoneAlias( IANA_Name );
            If FirstIndex >= 0 then
            Begin
              FirstIndex := AliasIndexToCZoneIndex( FirstIndex );
              EnterEntry(aWindowsTimeZones, IANA_Name, ISO3166_Name, Windows_Name, FirstIndex );
            end// If FirstIndex >= 0 then
            else
            Begin
              ShowMessage( IANA_Name + '@' + ISO3166_Name + '@' + Windows_Name );
            end;// If FirstIndex >= 0 then // Else
          end;// If FirstIndex >= 0 then // Else
        end;
      End;
    End;
  End;

  procedure WriteIncludeFile(aWindowsTimeZones:TList<TWindowsTimeZone>);
  var sOut:String;
  begin
    var sw := TStreamWriter.Create(
                               TPath.Combine( TPath.GetDirectoryName(paramstr(0)), 'WindowsTimezones.inc'),
                               False,{Append=False}
                               TEncoding.UTF8
                              );
    try
      SW.WriteLine('var');
      SW.WriteLine(' { This array contains Window Timezone aliases. }');
      SW.WriteLine(' CWindowsTimeZones: array[0 .. '+(aWindowsTimeZones.count-1).tostring+'] of TWindowsTimeZone = (');
      for var i:Integer := 0 to aWindowsTimeZones.count-1 do
      Begin
        sOut := Format('   (Index:%d; Windows_Name:%s; IANA_Name:%s; ISO3166_Name:%s; CZoneIndex:%d ),',
                             [aWindowsTimeZones[i].Index,
                              QuotedStr(aWindowsTimeZones[i].Windows_Name) ,
                              QuotedStr(aWindowsTimeZones[i].IANA_Name),
                              QuotedStr(aWindowsTimeZones[i].ISO3166_Name),
                              aWindowsTimeZones[i].CZoneIndex] );
        if i = aWindowsTimeZones.count-1 then
          sout := sOut.Remove( sOut.Length-1 );
        SW.Writeline( sOut );
      end;
      SW.WriteLine(   ');');
    finally
      SW.Free;
    end;
  End;

begin
  var WindowsTimeZones := TList<TWindowsTimeZone>.create;
  try
    ReadDocument(WindowsTimeZones);
    WriteIncludeFile(WindowsTimeZones);
  finally
    WindowsTimeZones.Free;
  end;
  ShowMessage('Document Done');
end;
Ich bekomme jetzt zumindest Crossplatform überall IANA Zeitzonen IDs...
Kein Mensch weiß warum Windows das von alleine nicht kann.

Hoffe es hilft jemand mit dem selben Problem. Ihr habt mir bei so vielen Problemen geholfen.

p80286 7. Feb 2020 20:52

AW: Lokale Windows Zeitzone in IANA format ermitteln
 
Könntest Du mal bitte das "IANA-Format" beschreiben? Ich kann dazu nichts finden.

Gruß
K-H

samso 8. Feb 2020 14:53

AW: Lokale Windows Zeitzone in IANA format ermitteln
 
Zeitzonen-Datenbank

p80286 8. Feb 2020 18:31

AW: Lokale Windows Zeitzone in IANA format ermitteln
 
Also wäre die Angabe für alle Rechner in Deutschland:
DE +5230+01322 Europe/Berlin +01:00 +02:00
und für Rechner in der Schweiz:
CH +4723+00832 Europe/Zurich +01:00 +02:00

Ist das richtig?

Gruß
K-H

QuickAndDirty 8. Feb 2020 19:54

AW: Lokale Windows Zeitzone in IANA format ermitteln
 
Aktuell ist das IANA-format meist
"Europe/Berlin"
"Europe/Paris"
"America/Buenos_Aires"
&c.

Aber die IANA hat auch Aliase für viele der Zeitzonen!
Und sie führt auch die historischen Namen der Zeitzonen und in welchem Zeitraum sie galten.
"Europe/Dublin"
hieß beispielsweise mal
"Eire"

Die Windows Zeitzonen sind zunächst mal sehr US-zentrisch. Da sind viele Bundesstaaten angegeben als Zeitzonen-Bezeichnung usw....
UND
sie sind aufs übelste lokalisiert! Und weder IOS noch Android nutzen die Windows Zeitzonen!!!
Ich weiß nicht was Linux nutzt :( aber da besteht evtl. noch Verbesserungsbedarf.

p80286 8. Feb 2020 20:00

AW: Lokale Windows Zeitzone in IANA format ermitteln
 
Danke!

K-H


Alle Zeitangaben in WEZ +1. Es ist jetzt 17:23 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz