Last active
November 6, 2025 16:40
-
-
Save ernstki/475186db21bad40d523454121faa2cb1 to your computer and use it in GitHub Desktop.
Convert LDAP timestamps (100-nanosecond intervals since 1 Jan, 1601) to human-readable times in your local timezone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| ## | |
| ## Convert LDAP’s “number of 100-nanosecond intervals since January 1, 1601 | |
| ## (UTC)” timestamps to human-readable dates, in the local timezone. See | |
| ## reference [1] for more details. | |
| ## | |
| ## Does not use f-strings, so should work in any old Python 3. | |
| ## | |
| ## Author: Kevin Ernst <ernstki -at- mail.uc.edu> | |
| ## Date: 13 Oct 2025 | |
| ## License: WTFPL or ISC, at your option | |
| ## Homepage: https://gist.github.com/ernstki/475186db21bad40d523454121faa2cb1/edit | |
| ## | |
| ## References: | |
| ## [1] https://learn.microsoft.com/en-us/windows/win32/adschema/a-badpasswordtime | |
| ## [2] https://www.loc.gov/item/today-in-history/november-18 | |
| import sys | |
| from datetime import (datetime as dt, timedelta as td, timezone as tz) | |
| HOMEPAGE = 'https://gist.github.com/ernstki/475186db21bad40d523454121faa2cb1/edit' | |
| def hectonanosecs_to_datetime(hectonanosecs): | |
| # for example: 134048391507850787 | |
| epoch = dt(year=1601, month=1, day=1, tzinfo=tz.utc) | |
| seconds_since_epoch = int(hectonanosecs) * 100 / 1e9 # secs since 1/1/1601 | |
| delta = td(seconds=seconds_since_epoch) | |
| return epoch + delta | |
| def hectonanosecs_to_human_date(hectonanosecs, raises=False): | |
| dt = hectonanosecs_to_datetime(hectonanosecs) | |
| try: | |
| timestamp = dt.astimezone() | |
| return str(timestamp) | |
| except ValueError as e: | |
| # timedelta doesn't have information do timezone-aware math for dates | |
| # before 1883, because without this you get this error: | |
| # | |
| # ValueError: offset must be a timedelta representing a whole | |
| # number of minutes, not datetime.timedelta(-1, 68638) | |
| # | |
| # If you're curious _why_, reference [2], a "Today in History" | |
| # article from the Library of Congress, has your answer: | |
| # | |
| # > On November 18, 1883, precisely at noon, North American railroads | |
| # > switched to a new standard time system for rail operations, which | |
| # > they called Standard Railway Time (SRT). Almost immediately after | |
| # > being implemented, many American cities enacted ordinances, | |
| # > thus resulting in the creation of time “zones.” The four | |
| # > standard time zones adopted were Eastern, Central, Mountain, | |
| # > and Pacific. Though tailored to the railroad companies’ train | |
| # > schedules, the new system was quickly adopted nationwide, | |
| # > forestalling federal intervention in civil time for more than | |
| # > thirty years, until 1918, when daylight saving time was introduced. | |
| if raises: | |
| raise RuntimeError( | |
| "Invalid timestamp. Mininum supported value is" | |
| "89268227999999995 (1883-11-18 12:00:00-05:00)") from e | |
| else: | |
| return '<invalid timestamp>' | |
| if __name__ == '__main__': | |
| import argparse | |
| parser = argparse.ArgumentParser( | |
| description='Converts timestamps in hectonanoseconds to normal ' | |
| 'datestamps.', | |
| epilog="Hint: Can also be used as a filter, reading from the " | |
| "stdout of another program like 'ldapsearch'. When used " | |
| "this way, any sufficiently large number is deemed to be " | |
| "a timestamp, converted in-place, and highlighted in ANSI " | |
| "color. Problems? Leave a comment at {}".format(HOMEPAGE)) | |
| parser.add_argument('timestamps', nargs='*', metavar='TIMESTAMP', | |
| help='a timestamp in hectonanoseconds') | |
| parser.add_argument( | |
| '-C', dest='color', action='store_const', const='always', | |
| help="force color when used as a filter, even if stdout isn't a " | |
| "terminal (same as '--color=always')") | |
| parser.add_argument('--raise', dest='raises', action='store_true', | |
| help='raise detailed exceptions for bad timestamps instead of ' | |
| 'just printing "<invalid timestamp>"') | |
| parser.add_argument('-d', '--debug', action='store_true', | |
| help='enable debug logging') | |
| parser.add_argument('--color', dest='color', metavar='WHEN', | |
| help="control ANSI color highlighting; 'always' = use color even " | |
| "when stdout isn't a tty, 'never' = suppress color, even if " | |
| "stdout is a tty") | |
| args = parser.parse_args() | |
| if sys.stdin.isatty(): | |
| if not len(args.timestamps): | |
| parser.error('Expected one or more timestamps.') | |
| for timestamp in args.timestamps: | |
| timestamp = hectonanosecs_to_human_date(timestamp, args.raises) | |
| print(timestamp) | |
| sys.exit(1) | |
| import re, logging | |
| ansi_color = False | |
| boldblue = "\x1b[34;1m"; reset ="\x1b[0m" | |
| if args.debug: | |
| logging.getLogger().setLevel(logging.DEBUG) | |
| if (sys.stdout.isatty() and not args.color in \ | |
| ['0', 'no', 'off', 'none', 'never', 'false']) \ | |
| or args.color == 'always': | |
| ansi_color = True | |
| for line in sys.stdin.readlines(): | |
| line = line.strip() | |
| # if a timestamp is < 17 digits and it's before time zones existed, so | |
| # don't even bother; in the high 18 digits it's like 4,500 CE, and | |
| # after 19 digits Python's datetime library wraps back to the 1900s | |
| matches = re.findall(r'\b\d{17,18}\b', line) | |
| if not matches: | |
| logging.debug("No match for line '%s'" % line) | |
| logging.debug("matches: %s", matches) | |
| print(line) | |
| continue | |
| for match in matches: | |
| timestamp = hectonanosecs_to_human_date(match, args.raises) | |
| if ansi_color: #and not match == line: | |
| timestamp = r"%s%s%s" % (boldblue, timestamp, reset) | |
| line = line.replace(match, timestamp) | |
| print(line) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Before:
After: