<span class="mw-page-title-namespace">Blog</span><span class="mw-page-title-separator">:</span><span class="mw-page-title-main">Hacks/Anno Fabri</span>
Fabrice P. Lauss𝕪s oomers' Web

Anno Fabri

Since 6 May (2005), I've been using a simple script—uniqname—to generate a unique name to stamp or tag various things without having to give it a second thought. The insight is to use the time at which I need this (I'm a clever boy, ain't I?) So this is the uniqname for now:

laussy@azag:~$ uniqname 
TueAug18100559CEST2026

Problems: It's ugly, it's not memorable, it's big.

For a long time, I had as a pet project to make my own compressor to make it a memorable, cute little tag which would not lose the information. For there is some information, think about it! All my documents since May 2005 are sprinkled with when I generated an equation, tagged something, etc. For someone like me that works on projects that run over entire decades—such as Microcavities—not to say most of my papers anyway, that a gold mine of information. I didn't process that yet, but at some point, I will.

Now, AI makes such a necessary but everyday-postponed task the matter of a few seconds... much less time to do than to write this post!

Meet Anno Fabri.

I restricted its range so that it fits on five alphanumerical characters, which is almost small enough to remember in short-term memory. So it runs between 18 August (2026) midnight (all of today)—which therefore becomes Anno Fabri, year of the maker——and 29 August (2055) at 09:20 in the morning. I hope I'll be dead by then, so as to not having to worry about my calendar collapsing. That is not an arbitrary stop: five alphanumerical characters spell 625 = 916,132,832 different things, and 916,132,832 seconds is 29.03 years. One second of my life from now onward, one tag, and the last one falls on that August morning. The general layout is:

E₁E₂(ABC)

Where E₁E₂ are two "era" numbers that label the time passing since Anno Fabri:

  • E₁ increases every 62 E₂ eras = 170.9 days or every 5.6 months: slightly over twice a year.
  • E₂ increases every 2 d 18 h 12 m 08 s
  • ABC is the key that labels the rest, and is jumbled so as to become "memorable" as opposed to smoothly increasing, as it would be prone to confusion. The jumble is keyed on the era itself, so that the same distance into two different eras gives unrelated letters—otherwise ABC would be one fixed table replayed 3844 times, and any two tags 2 d 18 h 12 m 08 s apart would end alike.

We are 00Yps (af) af as I write this! Isn't it memorable? Mnemotechnics is your friend, it works great for short term memory (what you need to label equations). Yps is like Oops but with Y and you know how much I've been obsessed with 𝕐 recently... so that's it... I'll never forget this 00Yps AF, where AF is, of course, Anno FabriFabrice's time!

To go the other way, to figure out when is exactly 00Yps, use

laussy@azag:~$ AF 00Yps
1787044261      TueAug18111101CEST2026  Tuesday 18 August 2026, 11:11:01 CEST

Why an era lasts 2 d 18 h 12 m 08 s

The E₂ duration is forced, and rather pleasingly so. The three jumbled characters ABC can name 623 = 238,328 distinct instants which is how many seconds we get. Use them wisely! The two era characters E₁E₂ can count 622 = 3,844 eras, and those 3,844 eras have to cover the whole 916,132,832-second window, so an era must be at least 238,328 seconds long. The two bounds meet at the same number, and 238,328 seconds is 2 d 18 h 12 m 08 s. There is nothing to tune: the five characters are used to the last one, with no spare capacity anywhere.

A consequence is that eras become their own calendar objects. Only era 00 begins at midnight, because that is where the counting starts. The counting goes:

era begins at
00 00:00:00
01 18:12:08 +18 h 12 m 08 s, or −5 h 47 m 52 s
02 12:24:16
03 06:36:24
04 00:48:32 back near midnight, 48 m 32 s past it

And then there is a drift of 48:32 minutes which accumulates. It'll never reset in my lifetime: era 3709—beginning Saturday 22 August (2054) at 00:02:32—will be two and a half minutes past midnight, twenty-eight years from now, and only eight days before the whole scheme runs out! A near-perfect return, arriving just too late to matter.

The jumbling

The last three characters are simply the position of the second inside its era—but written in a scrambled order, since consecutive seconds must not look alike to avoid confusion ("Bug" and "007" are more memorable than "Xyf" and "Xye"). The requirement is delicate: the scrambling must be a genuine permutation of the 238,328 positions, each position going to exactly one other, or the tag would stop being reversible and it would become lossy.

The position is cut into two unequal halves, one worth 62 values and the other 3,844. Then, six times in a row, one half is left untouched while the other has added to it a hash of the untouched one, keeping only the remainder of the division. Each such step is undoable on its own: on the way back, the half that served as input is still sitting there intact, so the same hash can be recomputed and subtracted again. The halves take turns, so after six passes each has been stirred by the other three times over. Nothing is ever discarded, hence the chain of six is as reversible as each of its links—and since a single second of difference feeds a different number into the very first hash, everything downstream comes out unrecognisable.

It works about as well as it possibly could: over a long run of consecutive seconds, 2.950 of the three characters change on average, where the theoretical maximum is 2.952. In practice, that means all three change, essentially always! «Oh, happy Meat. Oh, happy Soul. Oh, happy Fabrice Lauss𝕪.»

Timezone and Degenerate tags

Sometime I'm not in Europe and the timezone touches my time more hardly than it does myself. This is not stored by default as it breaks the 5-character limit for moderately little gain. So what I do is that if I am in a different time-zone, then a sixth letter is added to the 5 by default for my default timezone which is Europe/Madrid (until further notice). The letters are:

A=−11:00  B=−10:00  C=−09:30  D=−09:00  E=−08:00  F=−07:00  G=−06:00
H=−05:00  I=−04:00  J=−03:30  K=−03:00  L=−02:30  M=−02:00  N=−01:00
O=+00:00  P=+01:00  Q=+02:00  R=+03:00  S=+03:30  T=+04:00  U=+04:30
V=+05:00  W=+05:30  X=+05:45  Y=+06:00
c=+08:45  d=+09:00  e=+09:30  f=+10:00  g=+10:30  h=+11:00  i=+12:00
j=+12:45  k=+13:00  l=+13:45  m=+14:00

Tags are, like uniqname, accurate to the second. This has never been a problem, as I'm not so fast as to require to tag two things within the samtoo sure as maybe there'll be someautomization that will bring us there. That's again much more characters than it'd be needed, so I also reserve the numbers 0‒9 for 1/10s liftin taking the first 91ms, that gives us asmuch granularity. It could be more if I also use the 13 sixth letters left free from the timezone: 42ms accuracy.

The code

#!/bin/bash
# AF — v7.1.0 — Anno Fabri: tiny lossless timestamp tag, 5 chars, base62
#   chars 1-2 : era = 2.76-day block since the Anno Fabri epoch (2026-08-18 00:00 local), plain counter
#   chars 3-5 : position in the era, through a 6-round unbalanced Feistel over Z62 x Z3844,
#               tweaked by the era itself (TW=hi*SALT) so no two eras share a jumble
#               -> every second changes ~all three (bijective, so still lossless)
#   char    6 : UTC offset, PRESENT ONLY when the tag is made away from home. A LETTER,
#               digits being reserved: index into OFF, the 39 offsets in use on Earth,
#               west to east, A=-11:00 .. Q=+02:00 .. m=+14:00, 13 letters left spare
# Window 2026-08-18 .. 2055-08-29 (62^5 seconds exactly).   F.P. Laussy
#
#   AF [WHEN]            tag for now, or for a date: AF 3 september 2026 15:10
#                        (a bare 9-11 digit number, or @N, is read as an epoch)
#   AF TAG...            back to the time it stands for -- a lone 5- or 6-character
#                        word that no date reading fits is taken as a tag
#   AF -b|--back TAG...  the same, forced (for today, Friday, Monday...)
#   AF -v|--version      version;  AF -h|--help  this
LC_TIME=C
NAME=${0##*/}
HOME_TZ=${AF_TZ:-Europe/Madrid}
A=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
ZL=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
OFF=(-39600 -36000 -34200 -32400 -28800 -25200 -21600 -18000 -14400 -12600 -10800 -9000 -7200 -3600 0 3600 7200 10800 12600 14400 16200 18000 19800 20700 21600 23400 25200 28800 31500 32400 34200 36000 37800 39600 43200 45900 46800 49500 50400)
BASE=1787004000 N=916132832 ERA=238328 a=62 b=3844 SALT=232959
K=(0x2f4b6c1d 0x7a19e3b5 0x5c83d271 0x1e6fa94b 0x3d5e8a17 0x64c2b90f)
usage(){ sed -n "2s/^# //p; /^#\$/,/^[^#]/{/^#   /{s/^#   //; s/\bAF\b/$NAME/g; p}}" "$0"; exit ${1:-0}; }
f(){ local h=$(( ($1 ^ ${K[$2]} ^ TW) * 0x9E3779B1 & 0xFFFFFFFF ))
     h=$(( (h ^ (h>>15)) * 0x85EBCA6B & 0xFFFFFFFF ))
     h=$(( (h ^ (h>>13)) * 0xC2B2AE35 & 0xFFFFFFFF )); FR=$(( h ^ (h>>16) )); }
off(){ local z; printf -v z '%(%z)T' "$1"; o=$((10#${z:1:2}*3600+10#${z:3:2}*60)); [[ ${z:0:1} == - ]] && o=$((-o)); }
homeoff(){ local was=${TZ+1} old=${TZ-}; export TZ=$HOME_TZ; off "$1"; HO=$o
           if [[ $was ]]; then export TZ=$old; else unset TZ; fi; }
val(){ V=0; local c d j; for ((j=$2;j<$3;j++)); do c=${1:j:1}; d=${A%%$c*}
       [[ $d == "$A" ]] && { echo "$NAME: bad character '$c' in tag" >&2; exit 2; }; V=$((V*62+${#d})); done; }

back(){
  [[ $# -gt 0 ]] || usage 2
  for k; do
    [[ ${#k} == 5 || ${#k} == 6 ]] || { echo "$NAME: '$k' is not a 5- or 6-character tag" >&2; exit 2; }
    val "$k" 0 2; hi=$V; TW=$((hi*SALT)); val "$k" 2 5; L=$((V/b)) R=$((V%b))
    for i in 4 2 0; do f $L $((i+1)); R=$(( ((R-FR)%b+b)%b )); f $R $i; L=$(( ((L-FR)%a+a)%a )); done
    t=$((BASE+hi*ERA+L*b+R))
    if ((${#k}>5)); then c=${k:5:1}; d=${ZL%%$c*}
      [[ $d == "$ZL" || -z ${OFF[${#d}]} ]] && { echo "$NAME: '$c' is not an offset letter" >&2; exit 2; }
      o=${OFF[${#d}]}
      m=$(( (o%3600<0 ? -o%3600 : o%3600)/60 ))          # away: no name is recoverable, so
      if ((m)); then printf -v z '%+03d%02d' $((o/3600)) $m # spell the offset exactly as date's
      else printf -v z '%+03d' $((o/3600)); fi             # %Z does for a nameless zone
      printf -v zh 'UTC%s' "$z"
    else was=${TZ+1} old=${TZ-}; export TZ=$HOME_TZ
      homeoff "$t"; o=$HO; printf -v z '%(%Z)T' "$t"; zh=$z
      if [[ $was ]]; then export TZ=$old; else unset TZ; fi; fi
    export TZ=UTC                                  # render the stored wall clock verbatim
    printf -v u '%(%a%b%-d%H%M%S)T' $((t+o)); printf -v y '%(%Y)T' $((t+o))
    printf -v r '%(%A %-d %B %Y, %H:%M:%S)T' $((t+o)); unset TZ
    printf '%s\t%s%s%s\t%s %s\n' "$t" "$u" "$z" "$y" "$r" "$zh"
  done
}

# Is every argument shaped like a tag, and is there no date reading of the whole
# line? Then they are tags. Nothing that AF emits is a date GNU date will read
# -- 0 of 501 real tags across the window were -- so this never steals a tag;
# and the words that ARE both, "today" and "Friday" and their kind, keep their
# obvious meaning. Use -b to force the other reading.
astags(){
  local x
  (($#)) || return 1
  for x; do [[ $x =~ ^[0-9A-Za-z]{5,6}$ ]] || return 1; done
  ! date -d "$*" +%s >/dev/null 2>&1
}

case $1 in
-h|--help) usage ;;
-v|--version) sed -n '2s/^# //p' "$0"; exit 0 ;;
-b|--back|-d) shift; back "$@"; exit ;;
-*) echo "$NAME: unknown option $1" >&2; usage 2 ;;
*)
  astags "$@" && { back "$@"; exit; }
  if (($# == 0)); then t=$EPOCHSECONDS                      # now: no fork
  elif [[ $# == 1 && $1 =~ ^@?[0-9]{9,11}$ ]]; then t=${1#@}  # an epoch, as given
  else                                                       # anything else: let date read it
    t=$(date -d "$*" +%s 2>/dev/null) || { echo "$NAME: cannot read a date out of \"$*\"" >&2; exit 2; }
    said=1
  fi
  n=$((t-BASE))
  ((n>=0 && n<N)) || { echo "$NAME: $t outside 2026-08-18..2055-08-29" >&2; exit 1; }
  hi=$((n/ERA)) L=$(( n%ERA/b )) R=$(( n%ERA%b )); TW=$((hi*SALT))
  for i in 0 2 4; do f $R $i; L=$(( (L+FR)%a )); f $L $((i+1)); R=$(( (R+FR)%b )); done
  lo=$((L*b+R)) e= l=
  for ((i=0;i<2;i++)); do e=${A:hi%62:1}$e; hi=$((hi/62)); done
  for ((i=0;i<3;i++)); do l=${A:lo%62:1}$l; lo=$((lo/62)); done
  s=$e$l; off "$t"; here=$o; homeoff "$t"
  if ((here!=HO)); then
    q=-1; for i in "${!OFF[@]}"; do ((OFF[i]==here)) && { q=$i; break; }; done
    ((q<0)) && { echo "$NAME: offset $here s is not in OFF -- append it" >&2; exit 3; }
    s=$s${ZL:q:1}
  fi
  printf '%s\n' "$s"
  # a date typed by hand can be misread; when a human is watching, say what was
  # understood -- on stderr, so a pipe still receives the tag and nothing else
  if [[ $said && -t 1 ]]; then date -d "@$t" '+# %A %-d %B %Y, %H:%M:%S %Z' >&2; fi ;;
esac

The script is AF, with af as a symlink beside it, and it answers to whichever name it was called by—-h prints the usage, -v the version, both read out of the script's own header so they cannot drift. it is pure bash and forks nothing: the epoch comes from $EPOCHSECONDS and the timezone offset from printf '%(%z)T', which is a builtin, so date is never called—a tag takes a few milliseconds, essentially all of it spent starting the shell. With no argument it stamps the present moment, with an epoch it stamps that one instead, and -b (or --back) walks the other way, giving the epoch, the uniqname string the tag stands for, and the same thing spelled out for humans:

laussy@azag:~$ AF
00Yps
laussy@azag:~$ AF -b 00Yps
1787044261	TueAug18111101CEST2026	Tuesday 18 August 2026, 11:11:01 CEST
laussy@azag:~$ AF 1787044261
00Yps
laussy@azag:~$ AF 3 september 2026 15:10
065oP
# Thursday 3 September 2026, 15:10:00 CEST

The -b is not needed: a lone five or six character word that no reading as a date fits is taken as a tag and turned back into its moment. That is safe rather than lucky—of 501 real tags spread across the whole window, not one is something date will parse—while the few words that are both, today and Friday and their kind, keep the meaning anyone would expect of them. -b remains for insisting on the other reading, and AF -b today duly answers Saturday 5 October 2052. A date can also be typed in words rather than in seconds—anything date itself understands, from 3 september 2026 15:10 to next friday—and since a date typed by hand is a date that can be misread, the reading is echoed back on the second line whenever a human is watching. Piped into something, the tag comes out alone. Out of range, it refuses rather than wrapping around silently, which after 29 August 2055 will be somebody else's problem. Or, better put, it'll be nobody's problem!