Convert between Windows and Unix epoch with Python and Perl

From Svendsen Tech PowerShell Wiki
Jump to: navigation, search

Sometimes as a developer or IT guy you will come across Windows epoch hundredth nanosecond time stamps (such as AD timestamps). I wrote these pieces of Python and Perl code a couple of years ago when I was dealing with Active Directory time stamps and needed to convert to Unix epoch to do some date calculations in Perl. I have since then also put up a Python version.

There's Python code here, and Perl code here.

The Python code demonstrates how to convert between Unix epoch and Windows epoch, both ways.

The functions win_to_unix_epoch() and unix_to_win_epoch(), available for scrutiny in the Perl source code, demonstrate how to convert between Windows and Unix epoch, both ways.

There's a "magic number" of seconds that makes up the difference between the Windows and Unix epoch (11644473600).

If you provide a nanosecond timestamp, get a date, and convert it back to a nanosecond timestamp, you will notice that you lose precision beyond seconds. This is a limitation due to how the programs work with seconds at some points.




Perl Version

Integrated Help Text

PS C:\> .\Conv-WinTime.exe
Usage: Conv-WinTime.exe [--help] [--nano2human "100th nanoseconds"]
                    [--human2nano "year-month-day hour:minute:second"]

--help       : Print this help text.
--nano2human : Enter 100th nanoseconds as used by for instance the
               lastLogon LDAP/AD attribute on Windows, and get a human-
               readable date back.
--human2nano : Enter a human-readable date in the format:
               "year-month-day hour:minute:second", and get the 100th
               nanosecond timestamp back.

Author: Joakim Svendsen, "joakimbs" using Google's mail service.

ERROR: Please specify either --nano2human, --human2nano or both

Example Use Of Perl Conv-WinTime Version

PS C:\> perl .\Conv-WinTime.pl --human2nano '2012-12-24 00:00:00'
Date: 2012-12-24 00:00:00 | 100th ns (Wintime): 130007772000000000

And to convert it back:

PS C:\> .\Conv-WinTime.exe --nano2human 130007772000000000
100th ns (Wintime): 130007772000000000 | Date: 2012-12-24 00:00:00

To get a little more fancy, and involve PowerShell, you can convert multiple dates to nanosecond timestamps (or the other way around) like this (or similarly):

PS C:\> '2012-12-24 00:00:00', '2013-12-24 00:00:00' | %{ .\Conv-WinTime.exe --human2nano $_ }
Date: 2012-12-24 00:00:00 | 100th ns (Wintime): 130007772000000000
Date: 2013-12-24 00:00:00 | 100th ns (Wintime): 130323132000000000

A custom-formatted Get-Date will also do:

PS C:\> Get-Date -uformat '%Y-%m-%d %H:%M:%S' | %{ .\Conv-WinTime.exe --human2nano $_ }
Date: 2012-03-17 15:05:30 | 100th ns (Wintime): 129764667300000000

To get just the nanoseconds without all the other stuff, you can do it in a ton of ways. This one's probably the simplest, using the -split operator and array indexing to get the last whitespace-separated element, indexed by [-1]:

PS C:\> '2012-12-24 00:00:00', '2013-12-24 00:00:00' |
>> %{ ((.\Conv-WinTime.exe --human2nano $_) -split '\s+')[-1] }
>>
130007772000000000
130323132000000000

Perl Downloads


Perl Source Code

use warnings;
use strict;
use Getopt::Long;
use File::Basename;
use Time::Local;

our $VERSION = '1.01';
my $debug = 1;


main();

sub main {
    my %opt;
    GetOptions('help'          => \$opt{help},
               'nano2human=s'  => \$opt{nano2human},
               'human2nano=s'  => \$opt{human2nano},
               ) or die "Errors encountered while parsing command line options: $! -- $^E\n";
    parse_args(\%opt);
    if ($opt{nano2human}) {
        print_help("Malformed nano time (non-digits found): $opt{nano2human}\n") if $opt{nano2human} =~ /\D/;
        nano2human($opt{nano2human});
    }
    if ($opt{human2nano}) {
        human2nano($opt{human2nano});
    }
}


sub print_help {
    my @message = @_;
    my $script_name = basename $0;
    print <<EOF;
$script_name v$VERSION

Usage: $script_name [--help] [--nano2human "100th nanoseconds"] 
                    [--human2nano "year-month-day hour:minute:second"]

--help       : Print this help text.
--nano2human : Enter 100th nanoseconds as used by for instance the
               lastLogon LDAP/AD attribute on Windows, and get a human-
               readable date back.
--human2nano : Enter a human-readable date in the format:
               "year-month-day hour:minute:second", and get the 100th
               nanosecond timestamp back.

Author: Joakim Svendsen, "joakimbs" using Google's mail service.

EOF
    
    print @message, "\n" if @message;
    exit;
}


sub nano2human {
    my $wintime = shift;
    my $unix_epoch = win_to_unix_epoch($wintime);
    my ($year, $month, $day, $hour, $minute, $second) = (localtime $unix_epoch)[5,4,3,2,1,0];
    $year  += 1900;
    $month += 1;
    ($month, $day, $hour, $minute, $second) = map { sprintf '%02d', $_ } $month, $day, $hour, $minute, $second;
    print "100th ns (Wintime): $wintime | Date: " . join('-', $year, $month, $day) . ' ' . join(':', $hour, $minute, $second);
    print "\n";
}


sub human2nano {
    my $date = shift;
    if ($date =~ /^(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)$/) {
        my ($year, $month, $day, $hour, $minute, $second) = ($1, $2, $3, $4, $5, $6);
        # Conform to localtime() standards as used by Time::Local
        $year   -= 1900;
        $month  -= 1;
        my $unix_epoch = timelocal($second, $minute, $hour, $day, $month, $year);
        my $win_epoch = unix_to_win_epoch($unix_epoch);
        print "Date: $date | 100th ns (Wintime): $win_epoch\n";
    }
    else {
        print_help(qq{ERROR: Malformed date specified: $date\nValid example: '2000-12-24 20:00:00'.\nRemember double quotes ("") around the --human2nano argument});
    }
}


sub parse_args {
    my $opt = shift;
    print_help('') if $opt->{help};
    unless ($opt->{nano2human} or $opt->{human2nano}) {
        print_help("Warning: Please specify either --nano2human, --human2nano or both\n");
    }
}


sub win_to_unix_epoch {
    # Actually hundreths of nanoseconds at this point...
    my $nanoseconds = shift;
    # Get seconds
    my $seconds = $nanoseconds / 10_000_000;
    # This magic number is the difference between Unix and Windows epoch.
    my $unix_epoch = $seconds - 11644473600;
    # Return the Unix epoch for use with localtime().
    return $unix_epoch;
}


sub unix_to_win_epoch {
    my $unix_epoch = shift;
    my $winseconds = $unix_epoch + 11644473600;
    my $win_epoch = sprintf '%.0f', ($winseconds * 10_000_000);
    return $win_epoch;
}

Python Version Of Conv-WinTime

The Python version is similar to the Perl version. It uses the time.mktime() function, so it assumes the current time locale / time zone. Use calendar.timegm() for UTC (see source code below).

Example Use Of Python Conv-WinTime Version

To convert a date to a Windows nanosecond (AD) timestamp with the Python script version, you specify it according to an ISO standard (I should change the Perl script to use the same standard...) that looks like this: yyyy-MM-ddTHH:mm:ss. An example would be "2013-12-31T17:00:00".

Here it is in action:

PS E:\Python> .\Conv-WinTime.py --date2nano '2013-01-01T00:00:00'
130014684000000000

Convert it back and verify it's correct:

PS E:\Python> .\Conv-WinTime.py --nano2date 130014684000000000
2013-01-01 00:00:00

Use PowerShell for this near "noop" operation ("it does nothing"), to further verify it works as expected:

PS E:\Python> [datetime]::FromFileTime( (.\Conv-WinTime.py --date2nano '2013-01-01T00:00:00') )

01 January, 2013 00:00:00

Python Downloads

Here's the script:

Python Source Code

#!/usr/bin/env python
import datetime
import calendar
import time
import sys
import re
from optparse import OptionParser

# Author: Joakim Svendsen, "joakimbs" using Google's mail services.
# Copyright (c) 2013. Svendsen Tech. All rights reserved.
# BSD 3-clause license.

parser = OptionParser()
parser.add_option("-n", "--nano2date", type="long", dest="nano_time",
                  help="Specify a Windows nanosecond (AD) timestamp to convert to a date/time.")
parser.add_option("-d", "--date2nano", dest="human_datetime",
                  help="Specify a date and time string in the format: \"yyyy-MM-ddTHH:mm:ss\" to be converted to a \
                  Windows nanosecond (AD) timestamp. Example: \"2013-01-01T17:00:00\"")

(options, args) = parser.parse_args()

if options.nano_time:
    seconds = options.nano_time / 10000000
    epoch = seconds - 11644473600
    
    dt = datetime.datetime(2000, 1, 1, 0, 0, 0)
    print dt.fromtimestamp(epoch)

if options.human_datetime:
    m = re.compile(r'^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)$').match(options.human_datetime)
    if m:
        dt = datetime.datetime(*map(int, m.groups()))
        #unix_timestamp = time.mktime(dt.timetuple())
        windows_timestamp = long((time.mktime(dt.timetuple()) + 11644473600) * 10000000)
        print windows_timestamp
    else:
        print "Invalid date format specified with --date2nano: " + options.human_datetime + \
            ".\nUse the --help parameter for more information."