PowerShell und DateTime

An allen Ecken und Enden werden Zeitstempel verwendet. Sei es im Eventlog, in Mails, in Logfiles, Snooper u.a. Allerdings gibt es gleich viele verschiedene Formate, wie Zeiten angezeigt werden. Je nach Land unterscheidet sich die Schreibweise doch stark, sei es in der Reihenfolge der Werte, die Bezeichnung von Monaten etc.

Der Typ DateTime

.NET und damit auch PowerShell kennt den Variablentyp "[datetime]", der generisch die Speicherung von Datum und Zeitwerten zulässt. Zudem gibt es mit dem Commandlet "Get-Date" eine Möglichkeit , z.B. das aktuelle Datum zu ermitteln. Ein einfacher Aufruf zeigt alle Properties.

PS C:\> get-date | fl *


DisplayHint : DateTime
DateTime    : Dienstag, 29. Oktober 2013 00:16:08
Date        : 29.10.2013 00:00:00
Day         : 29
DayOfWeek   : Tuesday
DayOfYear   : 302
Hour        : 0
Kind        : Local
Millisecond : 229
Minute      : 16
Month       : 10
Second      : 8
Ticks       : 635186025682291672
TimeOfDay   : 00:16:08.2291672
Year        : 2013

Das Commandlet hat auch noch einige Methoden, die Sie mit folgenden Befehl anzeigen können (Nur ein Auszug)

PS C:\> get-date | gm *

   TypeName: System.DateTime

Name                 MemberType     Definition
----                 ----------     ----------
Add                  Method         datetime Add(timespan value)
AddDays              Method         datetime AddDays(double value)
AddHours             Method         datetime AddHours(double value)
AddMilliseconds      Method         datetime AddMilliseconds(double value)
AddMinutes           Method         datetime AddMinutes(double value)
AddMonths            Method         datetime AddMonths(int months)
AddSeconds           Method         datetime AddSeconds(double value)
AddTicks             Method         datetime AddTicks(long value)
AddYears             Method         datetime AddYears(int value)
CompareTo            Method         int CompareTo(System.Object value), int CompareTo(datetime value), int IComparable.CompareTo..
Equals               Method         bool Equals(System.Object value), bool Equals(datetime value), bool IEquatable[datetime].Equ..
IsDaylightSavingTime Method         bool IsDaylightSavingTime()
Subtract             Method         timespan Subtract(datetime value), datetime Subtract(timespan value)
ToFileTime           Method         long ToFileTime()
ToFileTimeUtc        Method         long ToFileTimeUtc()
ToLocalTime          Method         datetime ToLocalTime()
ToLongDateString     Method         string ToLongDateString()
ToLongTimeString     Method         string ToLongTimeString()
ToOADate             Method         double ToOADate()
ToSByte              Method         sbyte IConvertible.ToSByte(System.IFormatProvider provider)
ToShortDateString    Method         string ToShortDateString()
ToShortTimeString    Method         string ToShortTimeString()
ToString             Method         string ToString(), string ToString(string format), string ToString(System.IFormatProvider pr..
ToUniversalTime      Method         datetime ToUniversalTime()
DisplayHint          NoteProperty   Microsoft.PowerShell.Commands.DisplayHintType DisplayHint=DateTime
Date                 Property       datetime Date {get;}
Day                  Property       int Day {get;}
DayOfWeek            Property       System.DayOfWeek DayOfWeek {get;}
DayOfYear            Property       int DayOfYear {get;}
Hour                 Property       int Hour {get;}
Kind                 Property       System.DateTimeKind Kind {get;}
Millisecond          Property       int Millisecond {get;}
Minute               Property       int Minute {get;}
Month                Property       int Month {get;}
Second               Property       int Second {get;}
Ticks                Property       long Ticks {get;}
TimeOfDay            Property       timespan TimeOfDay {get;}
Year                 Property       int Year {get;}
DateTime             ScriptProperty System.Object DateTime {get=if ((& { Set-StrictMode -Version 1; $this.DisplayHint }) -ieq  "..

Interessante Funktionen sind z.B. alle, die mit "Add" beginnen, weil damit recht einfach eine Zeitspanne addiert aber auch subtrahiert werden kann.

Formatierungszeichen

Das Format einer Zeit unterscheidet sich nach Land, Sprache und auch die generelle Aufmachung kann beeinflusst werden. Systemintern wird die Zeit natürlich universelle gespeichert aber über folgende Formatierungszeichen können Sie bei der Ausgabe auf einen Bildschirm oder sonstige lesbare Form Einfluss nehmen

d     Day of month 1-31
dd    Day of month 01-31
ddd   Day of month as abbreviated weekday name
dddd  Weekday name
h     Hour from 1-12
H     Hour from 1-24
hh    Hour from 01-12
HH    Hour from 01-24
m     Minute from 0-59
mm    Minute from 00-59
M     Month from 1-12
MM    Month from 01-12
MMM   Abbreviated Month Name
MMMM  Month name
s     Seconds from 1-60
ss    Seconds from 01-60
t     A or P (for AM or PM)
tt    AM or PM
yy    Year as 2-digit
yyyy  Year as 4-digit
z     Timezone as one digit
zz    Timezone as 2-digit
zzz   Timezone
fff   Hundertstel

Diese Zeichen kommen in der Folge noch zum Einsatz.

Dateinamen mit Zeit

Eine Häufige Anwendung bei mir ist die Anlage von Log-Dateien mit einem Zeitstempel. Sie kennen das eventuell von den Exchange Message Tracking Logs aber ganz sicher von den Logdateien ihres Webservers (IISLogs, Apache etc.). Hier hat man es sich angewöhnt z.B. pro Tag oder Stunde eine neue Datei anzulegen. das geht dann recht einfach mit 

# Format für Täglich
$logfile = ".\Applog-$(get-date -Format yyyyMMdd).log"

# Format für stündlich
$logfile = ".\Applog-$(get-date -Format yyyyMMddHH).log"

#Format für Sekunde
$logfile = ".\Applog-$(get-date -Format yyyyMMdd-HHmmss).log"

Über den Weg lassen sich auch Dateien recht einfach nach dem Alter sortieren und löschen.

Knifflig wird es nur, wenn sie in einem Skript mehrere Programme mit dem gleichen Logfile aufrufen und dieses eine Logdatei überschreiben

In dem Fall definiere ich eher ein Prefix mit dem Skriptname und Zeit und hänge pro Programm dann ein Suffix an, z.B.

$logfileprefix = "Skriptname-$(get-date -Format yyyyMMdd-HHmmss)"
adamsync.exe /import /log "$($logfileprefox)-import.log"
adamsync.exe /sync /log "$($logfileprefox)-sync.log"

Sortierbarer Timestamp

Immer wieder benötige ich die Funktion, einen Zeitstempel in eine Datei oder ein Paket zu schreiben. Jedes Mal nutze ich natürlich "Get-Datei" dazu und genauso oft überlege ich mir wie ich einen "lesbaren" Zeitstempel hinbekomme. Soll ich die Zeit als lokale Zeit speichern oder doch lieber UTC, um bei weltweiten Analysen einen Abgleich zu ermöglichen?. Auf der anderen erschwert das natürlich ein lokales Debugging, wenn man immer die Stunden addieren oder abziehen muss. Dann gibt es ja noch unterschiedliche Schreibweisen je Land, z.B. "Tag Monat Jahr" in Deutschland aber eben "Monat Tag Jahr" in den USA. Abhängig davon kann man die Logs dann auch mehr oder wenige gut sortieren und zusammenführen. Irgendwann bin ich dann über den Formatter "-o" gestolpert.

PS C:\> get-date
Montag, 17. Dezember 2018 15:11:30

PS C:\> get-date -Format o
2018-12-17T15:11:34.1552790+01:00

PS C:\> get-date -Format u
2018-12-17 15:11:44Z

# Und nun noch mal mit UTC-Zeitzone
# Achtung: Angeblich liefert "ToUniversalTime() in der Stunde der Zeitumschaltung falche Zeiten
((get-date).ToUniversalTime().tostring("u"))
2018-12-17 15:11:44Z

# Besser ist daher folgender Aufruf
([System.DateTime]::UtcNow).tostring("u")

Das mit "format u" generierte Datum erfüllt all meine Wünsche, wenn ich die Zeit vorher auf UTC konvertiere:

  • Lesbar
    Das Format ist durch Menschen einfach lesbar
  • Sortierbar
    Auch die Sortierung nach Jahr, Monat Tag Stunde(24 Schreibweise) Minute Sekunde funktioniert auch eine einfache Sortierung als Textdatei
  • Zeitzonentauglich
    Die angehängte Korrektur anhand der Zeitzone zeigt einfach auf, wie weit die Zeit von UTC abweicht
  • Rückverwandelbar
    Und eine Konvertierung von dem String in ein DateTime-Objekt ist mit Get-Time fehlerfrei ohne Verluste möglich

Es spricht für mich also nichts dagegen, wenn ich einfach diese Funktion nutze. Es gibt neben der Konvertierung bei "Get-Date" auch direkt die ToString-Funktion, die auch die gleichen Daten liefert

PS C:\> $timestamp=get-date
PS C:\> $timestamp
Montag, 17. Dezember 2018 15:45:12

PS C:\> $timestamp.ToString("o")
2018-12-17T15:45:12.2723844+01:00

Und in Gegenrichtung reicht einfach der Aufruf über get-date.

PS C:\> get-date "2018-12-17T15:45:12.2723844+01:00"

Montag, 17. Dezember 2018 15:45:12

String zu DateTime

Wie ich anfangs beschrieben habe, gibt es unterschiedlichste Schreibweise und Zeitinformationen darzustellen. Als Entwickler ist man natürlich daran informiert, solche "Strings" in strukturierte Datenformate zu überführen. Mit einer "Textversion" eines Zeitstempel lässt sich nicht rechnen oder vergleichen und Fehlinterpretationen sind je nach Sprach- und Landeseinstellung wahrscheinlich. Ein einfacher Weg ist die Konvertierung über die "ParseExact"-Methode bei der Ein String mit einer Parametrisierung zu einem DateTime übertragen werden kann

$timestamp = [datetime]::parseexact($matches.timestamp,"yyyy-MM-dd HH:mm:ss",$null)

Natürlich können Sie auch einfach einen String direkt übernehmen. Solange der String das passende Format" hat und z. B. die lokale Einstellungen übereinstimmen. Ansonsten sehen Sie einen Fehler wie den folgenden.

PS C:\> [datetime]$timesstamp = "20.12.2013"
Der Wert "20.12.2013" kann nicht in den Typ "System.DateTime" konvertiert werden. Fehler: "String was not recognized as a valid
DateTime."
In Zeile:1 Zeichen:1
+ [datetime]$timesstamp = "20.12.2013"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [], ArgumentTransformationMetadataException
    + FullyQualifiedErrorId : RuntimeException

Wenn Sie solch Tests machen, dann sollten sie natürlich Zahlen verwenden, die auch einen Fehler produzieren. Selbst auf einem deutschen Windows 7 erwartet die Konvertierung in der Form "MM.dd.yyyy"

PS C:\> [datetime]$timestamp = "12.20.2013"
PS C:\> $timestamp

Freitag, 20. Dezember 2013 00:00:00

Natürlich können auch Zeiten mit gesetzt werden, die sogar bis auf Millisekunden und Ticks auflösen.

DateTime to String

Aber auch umgekehrt ist es natürlich sehr interessant, aus einer DateTime-Struktur eine Textversion zu generieren, die in Dateien, Logfiles oder Bildschirmausgaben verwendet werden kann. Mühsam ist es natürlich, aus den einzelnen Properties sich einen String zusammen zu kopieren:

[datetime]$timestamp = "12.20.2013 14:56"
write-host "Zeitstempel:" +$timestamp.day + "." +$timestamp.month +"." +$timestamp.year

Viel einfacher geht das mit der "ToString"-Methode, der man ein kurzes Datumsformat oder sogar einen komplett eigenen Formatierungsstring übergeben kann. Hier nutze ich die Funktion "now", um die aktuelle Zeit anders zu formatieren

([datetime]::now).tostring("dd.MM.yyyy HH:mm:ss.fff")

Wer z.B. einen Zeitstempel analog zu den IISLogs erzeugen will, kann folgende Zeile verwenden:

([datetime]::UtcNow).tostring("yyyy-MM-dd HH:mm:ss")

Eine Besonderheit sind natürlich Sonderzeichen. So benötigt der Lync Snooper das Datum durch "/" getrennt und die Zeit mit einem "|" abgetrennt. Das sieht dann wie folgt aus:

$timestamp.tostring("MM\/dd\/yyyy|HH:mm:ss.fff") +" 0000:0000 TRACE :: "+ $logline

Der "/" muss also durch das Escape-Zeichen "\" gesondert behandelt werden, da ansonsten ein "." ausgegeben wird. Mit dieser einfachen Funktion ist es natürlich sehr einfach, ein passendes Datum/Zeit-Format auszugeben

Addieren, Subtrahieren und Vergleichen

Mit Strings kann man nicht rechnen, aber mit DateTime schon. Intern wird der Zeitstempel "serialisiert" so dass Sie problemlos zwei Zeitstempel vergleichen und miteinander verrechnen können. Bei Letzterem ist das Ergebnis wieder ein DateTime

Diese Funktion ist besonders hilfreich, wenn Sie z.B. beim Exchange Message Tracking die Mails der letzten 10 Minuten erhalten wollen.

get-messagetrackinglog -start (get-date).addminutes(10)

Auch beim Vergleich von Datei-Zeitstempeln um sehr alte Dateien zu entfernen, ist dies hilfreich.

foreach ($file in (get-childitem ".\" | where {$_.lastwritetime -lt ( (get-date).adddays(-30))} )) {
	write-host ("Purging old log::" + $file)
	remove-item -path $file
}

Solche schnellen Lösungen waren als Batch fast gar nicht möglich und in VBScript war durchaus mehr Code zu schreiben.

Sortieren mit DateTime und Ticks

Das Commandlet "Sort-Object" erlaubt das Sortieren von Listen und kann nicht nur numerische Werte, sondern auch Zeitwerte sortieren. Ein einfacher Test geht mit einem Verzeichnis. Hier ein Auszug meines TEMP-Verzeichnis:

PS C:\> Get-ChildItem c:\temp\t*

    Verzeichnis: C:\temp

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        15.04.2014     12:25            temp
-a---        13.02.2013     00:11        334 tes2.csv
-a---        13.02.2013     00:15       5226 tes2.xml
-a---        18.10.2013     10:08      44924 test.cfg.ofg
-a---        23.11.2013     13:53        122 test.csv
-a---        18.03.2013     17:48       1758 test.htm
-a---        21.03.2013     18:30      15323 topo.tbxml

Sie sehen gut, dass das LastWriteTime-Feld unsortiert ist. Und nun das ganze "sortiert"

PS C:\> Get-ChildItem c:\temp\t*  | Sort-Object -Property LastWriteTime

    Verzeichnis: C:\temp

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        13.02.2013     00:11        334 tes2.csv
-a---        13.02.2013     00:15       5226 tes2.xml
-a---        18.03.2013     17:48       1758 test.htm
-a---        21.03.2013     18:30      15323 topo.tbxml
-a---        18.10.2013     10:08      44924 test.cfg.ofg
-a---        23.11.2013     13:53        122 test.csv
d----        15.04.2014     12:25            temp

Measure-Object mit DateTime

Wenn Sie in einer Liste mehrere Elemente mit einem DateTime-Wert haben, dann kann die Anforderung bestehen, auch darüber z.B. das älteste oder jüngste Datum oder den Durchschnitt zu ermitteln. Measure-Objekt kann leider nicht mit DateTime umgehen, aber natürlich mit [long]-Werten. Hier hilft es dann mit der "TICKS"-Funktion den Datumwert in Ticks seit 1.1.0001 umzurechnen. Hier am Beispiel eine Exchange 2007 Überwachung, bei der in alles Mails in den Queues die älteste Mail gesucht wird.

get-date ([long]((get-transportserver `
   | Get-Message ` 
   | %{$_.datereceived.ticks} 
   | Measure-Object -Maximum).maximum))

Das ist deutlich einfacher, übersichtlicher und schneller als selbst durch die Elemente zu laufen und jeden Eintrag mit dem aktuellen Maximum zu vergleichen.

Ein ähnlicher Anwendungsfall nutze ich, wenn ich Exchange Transaktionsprotokolle prüfe, um das älteste und neueste Log zu ermitteln, und dann über die Anzahl die durchschnittliche Aktivität errechne.

Kalenderwoche

Bei der Ermittlung der Woche im Jahr orientiert sich PowerShell natürlich an den regionalen Einstellungen des Betriebssystems. Das passt also nicht immer. Sie können die Woche ganz einfach mit folgendem Befehle ermitteln

# nicht immer korrekt
Get-Date -UFormat %V

Besser ist folgender Aufruf, damit das Gebietsschema zuverlässig beachtet wird.

# Richtig für Deutschland
[System.Globalization.DateTimeFormatInfo]::CurrentInfo.Calendar.GetWeekOfYear((get-date),2,1)

Der Fehler passiert natürlich nicht immer sondern es hängt von dem Betriebssystem und der dort hinterlegten Einstellung. Wer es nachprüfen will, kann folgendes Skript mal ausführen.

for ($count=0; $count -le 365; $count++) {
   $dayofyear = (get-date 01.01.2020).adddays($count)
   $weeka = get-date $dayofyear -UFormat %V
   $weekb = [System.Globalization.DateTimeFormatInfo]::CurrentInfo.Calendar.GetWeekOfYear($dayofyear,2,1)
   write-host "Day $($count) Date:$($dayofyear)  WeekA:$($weeka) WeekB:$($weekb) " -nonewline
   if ([int]$weeka -eq [int]$weekb) {
      write-host " OK" -foregroundcolor green
   }
   else {
      write-host " FAIL" -foregroundcolor red
   }
}

Bei mir erwischt es genau einen Tag im Jahr:

Wenn Sie also z.B. die Kalenderwoche zur Bildung von Dateinamen hernehmen, dann kann ihnen dieser Bug von "Get-Date -uFormat %V" ihre schöne Ausgabe vernageln.

UNIX Timestamp

In der *nix-Welt hat sich als Zeitstempel oft die Anzahl der Sekunden seit 1.1.1970 bewährt. Ich war damit z.B. beim Parsen der Ladehistorie eines BMW 225xe (BMW 225xe Erfahrungsbericht) konfrontiert.

$unixTimeStamp = "1619257274"

# Neu seit PowerShell 7.1
get-date -UnixTimeSeconds 1707140585

# vorher Variante 1
Get-Date 01/01/1970)+([System.TimeSpan]::fromseconds($unixTimeStamp))

# vorher Variante 2
(Get-Date 01/01/1970).AddSeconds($unixTimeStamp)  

# Beispiel
(Get-Date 01/01/1970)+([System.TimeSpan]::fromseconds(1619257274))

Samstag, 24. April 2021 09:41:14

Auch in der Gegenrichtung können Sie natürlich einfach die Sekunden seit dem 1.1.1970 errechnen. am 5. Feb 2024 gegen 13:42 waren das

# Neu seit PowerShell 7.1
[int64](get-date -uformat %s)

#Vorher
[int64]((get-date) - (get-date 01.01.1970)).totalseconds
1707140585

Weitere Links