uses format string approach for Z
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 12 Nov 2025 07:24:51 +0000 (07:24 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 12 Nov 2025 07:24:51 +0000 (07:24 +0000)
executable/Z

index 868aa49..604be49 100755 (executable)
-#!/usr/bin/env -S python3 -B
+#!/usr/bin/env python3
 # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
 
+"""
+Z – UTC timestamp helper (RT Code Format)
+
+Version: 3.4
+CLI parses args → calls work function → formats time.
+Safe for filenames.
+"""
+
 import sys
 import time
 import datetime
 import re
+from typing import Dict, Optional
 
-USAGE = """
-Z – UTC timestamp helper
-
-Usage:
-  Z                     Print default UTC timestamp (YYYY-MM-DD HH:MM:SS Z).
-  Z help                Show this help.
-  Z Unix                Print Unix time (seconds since epoch).
-  Z Unix-<seconds>      Print formatted UTC time for that Unix timestamp.
+# ----------------------------------------------------------------------
+# RT: 2-space indent, dense, no trailing spaces
+# ----------------------------------------------------------------------
+FIELDS = ["year","month","day","hour","minute","second","scintilla"]
+FIELD_INDEX = {name:i for i,name in enumerate(FIELDS)}
 
-  Z [OPTIONS]           Print a formatted UTC timestamp, where OPTIONS are:
-    T                   Use 'T' between date and time (ISO 8601 style).
-    T-<sep>             Use <sep> between date and time.
-                        T- with empty <sep> uses a single space (default).
+DEFAULT_FORMAT = "%year-%month-%day_%hour%minute%secondZ"
+ISO8601_FORMAT = "%year-%month-%dayT%hour:%minute:%second.%scintillaZ"
 
-    leftmost[-FIELD]    Leftmost field to include.
-                        FIELD: year, month, day, hour, minute, second, scintilla
-                        Default FIELD: year.
+VERSION = "3.4"
 
-    rightmost[-FIELD]   Rightmost field to include.
-                        Same FIELD set as leftmost.
-                        Default FIELD: second.
+USAGE = f"""
+Z – UTC timestamp helper (version {VERSION})
 
-    suffix              Use 'Z' as the suffix (default).
-    suffix-             Use 'UTC' as the suffix.
-    suffix-<s>          Use <s> as the suffix.
-    suffix-''           No suffix at all (literal two single-quotes).
+Usage:
+  Z                               Print default format
+  Z version                       Print version
+  Z iso | iso8601                 ISO 8601 with microseconds
+  Z format-"<fmt>"                Custom format string (one token)
+  Z Unix                          Unix seconds (now)
+  Z Unix-<sec>                    Use this time instead of system time.
+  Z help                          Show help
 
-    sed s/old/new[/g]   Apply sed-style substitution to the final output.
-                        Can be used multiple times.
+Fields:
+  %year %month %day %hour %minute %second %scintilla
 
 Examples:
-  Z T suffix-Z sed s/:/_/g
-  Z T- leftmost-day rightmost-minute suffix sed s/:/./g
-  Z sed s/:/-/g sed s/ /_/g suffix-''
-"""
-
-FIELDS = ["year", "month", "day", "hour", "minute", "second", "scintilla"]
-FIELD_INDEX = {name: i for i, name in enumerate(FIELDS)}
-
-DEFAULT_LEFTMOST = "year"
-DEFAULT_RIGHTMOST = "second"
-DEFAULT_SEP = " "
-DEFAULT_SUFFIX = " Z"
-
-# Regex to validate safe sed: only s/pattern/repl/[g]
-SED_RE = re.compile(r"^s/([^/]*)/([^/]*)(/(g?))?$")
-
-def print_usage():
-  print(USAGE.strip())
-
-def parse_field(label, value, default):
-  if not value:
-    return default
-  if value not in FIELD_INDEX:
-    print(f"Z: invalid {label} '{value}' (expected one of: {', '.join(FIELDS)})", file=sys.stderr)
-    raise SystemExit(1)
-  return value
-
-def build_timestamp(dt, leftmost, rightmost, sep, suffix):
-  li = FIELD_INDEX[leftmost]
-  ri = FIELD_INDEX[rightmost]
-  if li > ri:
-    print(f"Z: leftmost '{leftmost}' is finer than rightmost '{rightmost}'", file=sys.stderr)
-    raise SystemExit(1)
-
-  def in_range(name):
-    idx = FIELD_INDEX[name]
-    return li <= idx <= ri
-
-  # date part
-  date_parts = []
-  if in_range("year"):
-    date_parts.append(f"{dt.year:04d}")
-  if in_range("month"):
-    date_parts.append(f"{dt.month:02d}")
-  if in_range("day"):
-    date_parts.append(f"{dt.day:02d}")
-  date_str = "-".join(date_parts) if date_parts else ""
-
-  # time part
-  time_parts = []
-  want_hour = in_range("hour")
-  want_minute = in_range("minute")
-  want_second = in_range("second")
-  want_scint = in_range("scintilla")
-
-  if want_hour or want_minute or want_second or want_scint:
-    if want_hour:
-      time_parts.append(f"{dt.hour:02d}")
-    if want_minute:
-      time_parts.append(f"{dt.minute:02d}")
-    elif want_hour and (want_second or want_scint):
-      time_parts.append(f"{dt.minute:02d}")
-    if want_second or want_scint:
-      time_parts.append(f"{dt.second:02d}")
-
-  time_str = ":".join(time_parts) if time_parts else ""
-
-  if want_scint:
-    frac = f"{dt.microsecond:06d}"
-    if time_str:
-      time_str = f"{time_str}.{frac}"
-    else:
-      time_str = f"{dt.second:02d}.{frac}"
-
-  # combine date + time
-  if date_str and time_str:
-    out = f"{date_str}{sep}{time_str}"
-  else:
-    out = date_str or time_str or ""
-
-  # suffix
-  if suffix:
-    out = f"{out}{suffix}" if out else suffix
-
-  return out
-
-def format_utc(
-  *,
-  dt=None,
-  leftmost="year",
-  rightmost="second",
-  sep=" ",
-  suffix="Z",
-):
+  Z iso
+  Z format-"%year%month%day-%hour%minute%secondZ"
+  Z Unix-1731380000 iso
+  Z iso Unix-1731380000
+  Z Unix-1731380000 format-"%hour"
+""".strip()
+
+# ----------------------------------------------------------------------
+# Core: Get UTC time as dictionary
+# ----------------------------------------------------------------------
+def get_utc_dict(dt: Optional[datetime.datetime] = None) -> Dict[str,str]:
   if dt is None:
     dt = datetime.datetime.now(datetime.timezone.utc)
   elif dt.tzinfo is None:
     dt = dt.replace(tzinfo=datetime.timezone.utc)
   else:
     dt = dt.astimezone(datetime.timezone.utc)
-  return build_timestamp(dt, leftmost, rightmost, sep, suffix)
-
-def apply_sed_commands(text, sed_args):
-  """Apply a list of sed s///[g] commands safely."""
-  for sed_cmd in sed_args:
-    m = SED_RE.match(sed_cmd)
-    if not m:
-      print(f"Z: invalid sed command '{sed_cmd}' – must be s/old/new[/g]", file=sys.stderr)
-      raise SystemExit(1)
-    old, new, _, g_flag = m.groups()
-    flags = re.DOTALL if g_flag == "g" else 0
-    try:
-      text = re.sub(old, new, text, count=0 if g_flag else 1, flags=flags)
-    except re.error as e:
-      print(f"Z: invalid regex in sed '{sed_cmd}': {e}", file=sys.stderr)
-      raise SystemExit(1)
-  return text
-
-def CLI(argv=None):
+  return {
+    "year": f"{dt.year:04d}"
+    ,"month": f"{dt.month:02d}"
+    ,"day": f"{dt.day:02d}"
+    ,"hour": f"{dt.hour:02d}"
+    ,"minute": f"{dt.minute:02d}"
+    ,"second": f"{dt.second:02d}"
+    ,"scintilla": f"{dt.microsecond:06d}"
+  }
+
+# ----------------------------------------------------------------------
+# Core: Format time dictionary using format string
+# ----------------------------------------------------------------------
+_TOKEN_RE = re.compile(r'%([a-z]+)')
+
+def format_timestamp(time_dict: Dict[str,str], fmt: str) -> str:
+  def repl(m):
+    k = m.group(1)
+    return time_dict.get(k, m.group(0))
+  return _TOKEN_RE.sub(repl, fmt)
+
+# ----------------------------------------------------------------------
+# Work Function
+# ----------------------------------------------------------------------
+def make_timestamp(fmt: str = DEFAULT_FORMAT, dt: Optional[datetime.datetime] = None) -> str:
+  return format_timestamp(get_utc_dict(dt), fmt)
+
+# ----------------------------------------------------------------------
+# CLI – Thin wrapper, parses args, order-insensitive
+# ----------------------------------------------------------------------
+def CLI(argv=None) -> int:
   if argv is None:
     argv = sys.argv[1:]
 
-  # No-arg default
   if not argv:
-    out = format_utc(
-      leftmost=DEFAULT_LEFTMOST,
-      rightmost=DEFAULT_RIGHTMOST,
-      sep=DEFAULT_SEP,
-      suffix=DEFAULT_SUFFIX,
-    )
-    print(out)
+    print(make_timestamp())
     return 0
 
-  # Help
-  if argv[0] in ("help", "-h", "--help"):
-    print_usage()
-    return 0
+  # Quick handlers
+  if argv[0] == "version":
+    print(VERSION); return 0
+  if argv[0] in ("help","-h","--help"):
+    print(USAGE); return 0
 
-  # Z Unix
-  if argv[0] == "Unix" and len(argv) == 1:
-    print(int(time.time()))
-    return 0
-
-  # Unix-<seconds>
+  # Parse options in any order
+  fmt = DEFAULT_FORMAT
   dt_override = None
-  args = list(argv)
-  if args and args[0].startswith("Unix-"):
-    _, val = args[0].split("-", 1)
-    try:
-      sec = float(val)
-    except ValueError:
-      print(f"Z: invalid Unix seconds '{val}'", file=sys.stderr)
-      return 1
-    dt_override = datetime.datetime.fromtimestamp(sec, datetime.timezone.utc)
-    args = args[1:]
-
-  # Parse options
-  leftmost = DEFAULT_LEFTMOST
-  rightmost = DEFAULT_RIGHTMOST
-  sep = DEFAULT_SEP
-  suffix = DEFAULT_SUFFIX
-  sed_commands = []
 
-  i = 0
-  while i < len(args):
-    arg = args[i]
-
-    if arg == "T":
-      sep = "T"
-    elif arg.startswith("T-"):
-      tail = arg[2:]
-      sep = " " if tail == "" else tail
-
-    elif arg == "leftmost":
-      leftmost = "year"
-    elif arg.startswith("leftmost-"):
-      leftmost = parse_field("leftmost", arg.split("-", 1)[1], "year")
-
-    elif arg == "rightmost":
-      rightmost = "second"
-    elif arg.startswith("rightmost-"):
-      rightmost = parse_field("rightmost", arg.split("-", 1)[1], "second")
-
-    elif arg == "suffix":
-      suffix = "Z"
-    elif arg.startswith("suffix-"):
-      tail = arg.split("-", 1)[1]
-      if tail == "":
-        suffix = "UTC"
-      elif tail == "''":
-        suffix = ""
-      else:
-        suffix = tail
-
-    elif arg == "sed" and i + 1 < len(args):
-      sed_cmd = args[i + 1]
-      if not sed_cmd.startswith("s/"):
-        print(f"Z: 'sed' must be followed by s/old/new[/g], got '{sed_cmd}'", file=sys.stderr)
+  for arg in argv:
+    if arg in ("iso","iso8601"):
+      fmt = ISO8601_FORMAT
+      continue
+    if arg == "Unix":
+      # handled at print-time if no override set; leave as-is
+      continue
+    if arg.startswith("Unix-"):
+      try:
+        sec = float(arg.split("-",1)[1])
+      except ValueError:
+        print(f"Z: invalid Unix seconds in '{arg}'", file=sys.stderr)
         return 1
-      sed_commands.append(sed_cmd)
-      i += 1  # skip the next arg
-
-    else:
-      print(f"Z: unknown option '{arg}'", file=sys.stderr)
-      return 1
-    i += 1
-
-  # Build base timestamp
-  out = format_utc(
-    dt=dt_override,
-    leftmost=leftmost,
-    rightmost=rightmost,
-    sep=sep,
-    suffix=suffix,
-  )
-
-  # Apply sed transformations
-  if sed_commands:
-    out = apply_sed_commands(out, sed_commands)
+      dt_override = datetime.datetime.fromtimestamp(sec, datetime.timezone.utc)
+      continue
+    if arg.startswith("format-"):
+      fmt = arg[len("format-"):]
+      continue
+    # Unknown option
+    print(f"Z: unknown option '{arg}'", file=sys.stderr)
+    return 1
+
+  # Emit: if user asked for "Unix" with no override, print epoch seconds instead
+  if "Unix" in argv and dt_override is None and fmt == DEFAULT_FORMAT and len(argv) == 1:
+    print(int(time.time()))
+    return 0
 
-  print(out)
+  print(make_timestamp(fmt=fmt, dt=dt_override))
   return 0
 
+# ----------------------------------------------------------------------
+# Entry point
+# ----------------------------------------------------------------------
 if __name__ == "__main__":
   raise SystemExit(CLI())