Convert windows nanosecond epoch timestamp to human-readable date and vice versa

From Svendsen Tech Powershell Wiki

Jump to: navigation, search

Sometimes as a Windows IT administrator, you will come across Windows epoch hundredth nanosecond time stamps (AD timestamp - also related are so-called "Windows ticks"). I forgot why I wrote this app a couple of years ago, but I needed it at the time and I was dealing with Active Directory time stamps.

There's a pure PowerShell example here.

I'm now making it easily available. I originally wrote it in Perl, but have since then put up a Python version as well. Read more about Perl on Windows here and also check out Perl from PowerShell.

The functions win_to_unix_epoch() and unix_to_win_epoch(), available for scrutiny in the Perl source code, also 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 point.

Contents







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;
    
}

Pure PowerShell Approach

Here are some PowerShell examples for converting to and from AD timestamps based on the Windows NT epoch. The parsing of date strings apparently depends on locale settings, but something like the ISO standard yyyy-MM-ddTHH-mm-ss (year-month-day T 24hour-minute-second) should work reliably.

PS C:\> (Get-Date -Date "2013-12-24T00:00:00").ToFileTime()
130323132000000000

You can also cast to a datetime object and use the ToFileTime() method like this:

PS C:\> [datetime]'2013-12-31T21:00:00'

Tuesday, December 31, 2013 9:00:00 PM

PS C:\> ([datetime]'2013-12-31T21:00:00').ToFileTime()
130329936000000000

You can get "ticks" - which are from January 1st, year 1 - like this:

PS C:\> (Get-Date -Date "2013-12-24 00:00:00").Ticks
635234400000000000

To convert the AD time stamp back, you can use a static method, called FromFileTime(), on a temporarily instantiated DateTime object.

PS C:\> [DateTime]::FromFileTime('130323132000000000')

Tuesday, December 24, 2013 12:00:00 AM

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.

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."
Personal tools