Working on RSS git dates and lisp date format conversions
| link | Last. 20260118T061349263Z |
One request while I was missing for two weeks and not the first time I have received it was for me to finally provide RSS, thank-you. Here I will work on providing that in ansi common lisp for this blog.
Video livestream demo of the code and topics in this article https://gamerplus.org/@screwlisp/115914680173109415 Writing time conversions kind of dragged on, though it is interesting too in Naggum’s long painful history kind of way. I will extract the substance of this article into some focused and useable articles next week. This one is more of a history of me figuring it out.
I mention Naggum a few times since someone remembered his article to me while I was writing time stuff. If you do not know who Naggum is, https://en.wikipedia.org/wiki/Erik_Naggum he was an infamous Swedish lisp programmer usenet poster.
NOTE I hate advertisers, so I am going to use https://cyber.harvard.edu/rss/examples/rss2sample.xml as my reference for rss. https://www.rfc-editor.org/rfc/rfc822.html#section-5 is the (modified) time specification RSS uses due to Crocker in 1982.
RSS is an open standard for individual people to get new articles from individual websites, basically. The gist is that the individual’s RSS-reader program checks an XML file served by a website (/index.xml?) which has a list of the website’s articles. If the list has changed, the RSS-reader program downloads the new or changed articles for the person to read.
You can find out more about RSS from my friend MattoF: https://box.matto.nl/why-rss-is-important.html . Gonzalo Nemmi also had a link that the excuse for removing RSS was that XML is too old to use, debunked here: https://www.process-one.net/blog/stop-telling-us-xmpp-should-use-json/ . The gist is that XML is suitable for streaming through a file, not loading a whole dictionary structure into memory.
Albeit my blog index is currently generated from an Articles.json file. I guess I will load that with cl-json, and then write an index.xml RSS file using xhtmlambda. In this initial push I had to recover article gates from git which took up some space too.
I should mention Hajovonta has a json querying package cl-jsonpath, though the json I used here is small and uncomplicated.
It used to be that big companies offered RSS feeds from their users, but all the companies dumped it since it meant users could choose specific articles to get and did not have to interface with the company’s web applications (advertising and user data scraping). Similar to the XMPP story (did you know that around 2000 you used to be able to have friends who used different online chat service providers? Google, Microsoft, Yahoo, AOL, whatever all just offered the open standard XMPP (which they also got for free), and you could chat to people everywhere. Obviously, they all 3e’d their own chat services like they dropped support for RSS).
What I think RSS looks like and cl-json
| link | Last. 20260118T061402524Z |
Quicklisp, I guess.
(ql:quickload :cl-json)
(defun json-file-to-alist (path &optional (skip-lines 0))
(with-open-file
(in path)
(loop :repeat skip-lines :do (read-line in)) ; can remove a preamble
(json:decode-json in)))
Ooookay it turns out that json is like this: { "foo": "bar"} and not like this { foo:'bar' }, the latter being a briefly worked on json5.
This clearly works (now):
(defparameter *articles*
(json-file-to-alist #p"screwlisps-kitten/db/Articles.fragment.js" 1))
by the way, emacs slime users.
M-x slime-set-default-directoryto change directory and
((:LINK . "/momentary/a-python-tutorial-lisped/")
(:NAME . "Minor python and ansi cl slicing comparison with a lisp read macro")
(:DATE . "20260116T041740716896Z")
(:FEDI . "https://gamerplus.org/@screwlisp/115903097432314186"))
which I propose we can turn into
<rss version="2.0">
<channel>
<title>Screwlisp proposes kittens</title>
<link>https://screwlisp.small-web.org/</link>
<item>
<title>Minor python and ansi cl slicing comparison with a lisp read macro</title>
<link>/momentary/a-python-tutorial-lisped/</link>
<pubDate>16 Jan 2026 04:17:40 GMT</pubDate>
</item>
</channel>
</rss>
universal time dates ISO-8601 rfc882 RSS and fipa sl
| link | Last. 20260118T061413898Z |
Several weeks ago I settled on using the FIPA SL modification of ISO8601 for compatibility with my use of FIPA SL.
But now I need an RFC822 section 5 date for RSS!
First turning an ISO8601 date string into a universal time
(defun fipa-ISO8601-to-UT (string)
(let ((year (subseq string 0 4)) (month (subseq string 4 6)) (date (subseq string 6 8))
(hour (subseq string 9 11)) (minute (subseq string 11 13)) (second (subseq string 13 15))
(time-zone (case (char string (1- (length string)))
(#\Z "0"))))
(apply 'encode-universal-time
(mapcar 'parse-integer
(list second minute hour date month year time-zone)))))
Turning a universal time into an RFC882 section 5 date. Only Z supported, and a 4 digit year is used.
(defun UT-to-RFC882 (UT &aux (TZ 0))
(multiple-value-bind
(second minute hour date month year day)
(decode-universal-time UT TZ)
(format nil "~@{~?~^ ~}"
"~[Mon~;Tue~;Wed~;Thu~;Fri~;Sat~;Sun~]," `(,day)
"~2,'0d" `(,date)
"~[Jan~;Feb~;Mar~;Apr~;May~;Jun~;Jul~;Aug~;Sep~;Oct~;Nov~;Dec~]" `(,(1- month))
"~d" `(,year)
"~@{~2,'0d~^:~}" `(,hour ,minute ,second)
"~[Z~]" `(,TZ))))
(defun iso8601-datetime (tm)
(multiple-value-bind (second minute hour date month year day daylight-p zone)
(decode-universal-time (or tm (get-universal-time)) 0)
(format NIL "~4,'0D-~2,'0D-~2,'0D ~2,'0D:~2,'0D:~2,'0D +~2,'0D00" year month date hour minute second zone)
))
which has pretty formatting that my datetimes do not, because I am actually using the FIPA SL modification to ISO-8601. So you can see:
CL-USER> "20260116T041740716896Z"
"20260116T041740716896Z"
CL-USER> (iso8601-to-ut *)
3977525860
CL-USER> (iso8601-datetime *)
"2026-01-16 04:17:40 +0000"
CL-USER> (ut-to-rfc882 **)
"Fri, 16 Feb 2026 04:17:40 Z"
CL-USER>
but it is clear how they are all related. I set mdhughes’ timezone to 0 since the FIPA SL spec only allows Z (zulu = UTC).
The garbage at the end of my FIPA SL time is that it adds milliseconds at the end. Oh, oops, I now see I have given microseconds instead of milliseconds. Well, I will fix that going forward.
My fipa-sl from universal time:
(defun fipa-8601-from-ut+0 (ut)
(let ((decoded (multiple-value-list
(decode-universal-time ut 0))))
(format nil "~@{~?~}"
"~10,4,'0r" (list (sixth decoded))
"~10,2,'0r" (list (fifth decoded))
"~10,2,'0r" (list (fourth decoded))
"~a" (list #\T)
"~10,2,'0r" (list (third decoded))
"~10,2,'0r" (list (second decoded))
"~10,2,'0r" (list (first decoded))
"~10,3,'0r"
(list (rem (truncate (get-internal-real-time) 1000)
(truncate internal-time-units-per-second 1000)))
"~a" (list #\Z))))
alright, there we go.
CL-USER> "20260116T041740716896Z"
"20260116T041740716896Z"
CL-USER> (iso8601-to-ut *)
3977525860
CL-USER> (fipa-8601-from-ut+0 *)
"20260116T041740523Z"
CL-USER> (ut-to-rfc882 **)
"Fri, 16 Feb 2026 4:17:40 Z"
Now with milliseconds properly.
The connection to RSS was that RSS requires a form of the rfc882 one for <pubDate>.
Healing unknown times from git
| link | Last. 20260118T061423031Z |
Reid encouraged me on the Mastodon not to use placeholder or unknown pubDates in my RSS if I technically had an idea of when each article was published. And so I feel comfortable blaming this section of the article squarely on them, despite their protests. They have an incredible site banner.
Prior to starting to work on Aral’s kitten per se, I was just using kitten as a markdown site generator and I did not have a rigorous method of dating entries. I guess I can recover these from the git, last commit times anyway (which I will treat as an approximation of the pubDate rather than lastBuild in this case).
Sorry about the shell one-liner in uiop:run-program. Uiop is common lisp’s universal posix compatibility layer by Daniel Barlow etc.
(require "asdf")
(require "uiop")
(defun get-git-date (git-path path)
(uiop:run-program
(format nil
"~
cd ~a && git log -1 -- pretty=\"format%medium\" ~a ~
| grep -i date | cut -d' ' -f'5-'"
git-path path)
:output :string))
oh lucky me, another date format. By the way, Zyd linked us this piece of lisp time cultural history: https://naggum.no/lugm-time.html .
(defun git-time-to-rss (string)
"string like \"Dec 26 19:43:01 2025 +1300\"
returns (values rss-pubDate-string fipa-iso-8601-string iso-8601-string ut)
all in zulu time. ut is an integer."
(let* ((local-time
(format nil "~@{~?~}"
"~a" `(,(subseq string 16 20))
"~2,'0d" `(,(1+
(search `(,(subseq string 0 3))
'("Jan" "Feb" "Mar" "Apr" "May" "Jun"
"Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
:test 'string=)))
"~2,'0d" `(,(parse-integer (subseq string 4 6)))
"~a" '(t)
"~@{~2,'0d~}" (list
(parse-integer (subseq string 7 9) :junk-allowed t)
(parse-integer (subseq string 10 12) :junk-allowed t)
(parse-integer (subseq string 13 15)))
"000Z" '()))
(mag-offset (* (+ (* (parse-integer (subseq string 22 24)) 60)
(parse-integer (subseq string 24 26)))
60))
(ut (+
(fipa-iso8601-to-ut local-time)
(* mag-offset
(if (equal "+" (subseq string 21 22)) -1 +1)))))
(values
(ut-to-rfc882 ut)
(fipa-8601-from-ut+0 ut)
(iso8601-datetime ut)
ut)))
Oookay.
CL-USER> (get-git-date "~/screwlisps-kitten/" "index.page.js")
"Dec 26 19:43:01 2025 +1300
"
NIL
0
CL-USER> (git-time-to-rss *)
"Fri, 26 Dec 2025 06:43:01 Z"
"20251226T064301658Z"
"2025-12-26 06:43:01 +0000"
3975720181
CL-USER>
aanndd
(loop :with path := "screwlisps-kitten/"
:for article :in *articles*
:for date := (cdr (assoc :date article))
:for raw-link := (cdr (assoc :link article))
:for link := (subseq raw-link
(if (char= (char raw-link 0) #\/) 1 0)
(if (char= (char raw-link (1- (length raw-link))) #\/)
(1- (length raw-link))
(length raw-link)))
:for md := (concatenate 'string link ".page.md")
:for lisp := (concatenate 'string link ".lisp")
:for merged-link := (make-pathname :directory `(:relative ,path) :name link)
:for merged-md := (make-pathname :directory `(:relative ,path) :name md)
:for merged-lisp := (make-pathname :directory `(:relative ,path) :name link :type "lisp")
:for probe-link := (probe-file merged-link)
:for probe-md := (probe-file merged-md)
:for probe-lisp := (probe-file merged-lisp)
:for git-date := (cond (probe-link (get-git-date path link))
(probe-md (get-git-date path md))
(probe-lisp (get-git-date path lisp)))
:for my-date := (nth-value 1 (git-time-to-rss git-date))
:do (setf (cdr (assoc :date article)) my-date))
alright well, you may never know the difference but there used to be placeholder values on all the old articles, and now there are my FIPA SL modified ISO-8601 there. https://gamerplus.org/@screwlisp/115913872930011830
xhtmlambda titles dates and actually making rss
| link | Last. 20260118T061441593Z |
I feel like we are basically done. Marco Antoniotti offered his xhtmlambda for making the rss xml file on the Mastodon, so I am going to use that. Ugh, Marco, I am tired from the previous sections of my article and did not want to figure out how easy xhtmlambda is to extend, of which it is pretty easy, thank-you.
(ql:quickload :xhtmlambda)
(asdf:load-system :cl-json)
(in-package :xhtmlambda)
(defparameter *articles*
(json-file-to-alist
"screwlisps-kitten/db/Articles.fragment.js" 1))
(def-element rss :specific-attributes (:version))
(def-element channel)
(def-element title)
(def-element link)
(def-element item)
(def-element pubdate)
(def-element description)
(with-html-syntax-output
(t)
(rss
'(:version "2.0")
(channel
()
(title () "Screwlisp proposes kittens")
(link () "https://screwlisp.small-web.org")
(description () "Lisp, Common Lisp, computer history, programming, LambdaMOO, gopher, kitten, Mastodon, fediverse, the lispy gopher climate podcast, art writings by screwlisp (or screwtape; previously lived in the screwtape proposes a toast gopherhole)."
)
(ITEM
(TITLE NIL
"Minor python and ansi cl slicing comparison with a lisp read macro")
(PUBDATE NIL "Fri, 16 Jan 2026 04:17:40 Z")
(LINK NIL
"https://screwlisp.small-web.org//momentary/a-python-tutorial-lisped/"))
(ITEM (TITLE NIL "My lisp 2025 thanks and 2026 plans")
(PUBDATE NIL "Tue, 30 Dec 2025 02:26:05 Z")
(LINK NIL
"https://screwlisp.small-web.org//momentary/my-cl-2025-and-2026/")))))
well, this works:
<rss>
(VERSION 2.0)
<channel>
<title>
Screwlisp proposes kittens
</title>
<link>
https://screwlisp.small-web.org
</link>
<description>
Lisp, Common Lisp, computer history, programming, LambdaMOO, gopher, kitten, Mastodon, fediverse, the lispy gopher climate podcast, art writings by screwlisp / screwtape: previously lived in the screwtape proposes a toast gopherhole.
</description>
<item>
<title>
Minor python and ansi cl slicing comparison with a lisp read macro
</title>
<pubdate>
Fri, 16 Jan 2026 04:17:40 Z
</pubdate>
<link>
https://screwlisp.small-web.org//momentary/a-python-tutorial-lisped/
</link>
</item>
<item>
<title>
My lisp 2025 thanks and 2026 plans
</title>
<pubdate>
Tue, 30 Dec 2025 02:26:05 Z
</pubdate>
<link>
https://screwlisp.small-web.org//momentary/my-cl-2025-and-2026/
</link>
</item>
</channel>
</rss>
though I did not figure out how to nest the loop inside with-html-syntax-output properly yet but I can build the s-expression for it separately.
(loop
:with blog = "https://screwlisp.small-web.org/"
:for article :in (subseq *articles* 0 2)
:for title := (cdr (assoc :name article))
:for rellink := (cdr (assoc :link article))
:for link := (concatenate
'string
blog
(if (equal "/" (subseq rellink 0 1))
""
"/")
rellink)
:for date := (cdr (assoc :date article))
:for pubdate := (ut-to-rfc882
(fipa-iso8601-to-ut date))
:collect
`(item
(title () ,title)
(pubdate () ,pubdate)
(link () ,link)))
We have gone on for much too long! I will make one or several articles exploring this one in the next week, its useage, including ways I think you and others could sanely use the code. This article was me haphazardly making stuff work.
I will talk about time in lisp and RSS on the peertube live in two hours ( https://toobnix.org/w/gXLXQqxf5MYg1NDF2Ua6oA ) which will also be a debugging episode just in case something broke between last year and this.
(One hour) video livestream of the code in emacs useage: https://gamerplus.org/@screwlisp/115914680173109415