More
author |
Steve Losh <steve@stevelosh.com> |
date |
Tue, 09 Apr 2024 09:20:04 -0400 |
parents |
c5124aa20cbd |
children |
(none) |
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload '(:adopt :iterate :chronicity :local-time :losh)
:silent t))
(defpackage :when
(:use :cl :iterate :losh)
(:export :toplevel :*ui*))
(in-package :when)
;;;; Configuration ------------------------------------------------------------
(defparameter +iso-8601-seconds-format+
'((:year 4) #\- (:month 2) #\- (:day 2)
#\T
(:hour 2) #\: (:min 2) #\: (:sec 2)
:gmt-offset-or-z))
(defparameter *time-formats*
`((:rfc-3339 . ,local-time:+rfc3339-format+)
(:iso-8601 . ,local-time:+iso-8601-format+)
(:iso-8601-seconds . ,+iso-8601-seconds-format+)
(:gnuplot . ((:day 2) #\/ (:month 2) #\/ (:year 4) #\, (:hour 2) #\: (:min 2)))))
;;;; Errors -------------------------------------------------------------------
(define-condition user-error (error) ())
(define-condition need-one-time-string (user-error) ()
(:report "Exactly one time string is required."))
;;;; Functionality ------------------------------------------------------------
(defun jitter-ns (seconds)
(if (zerop seconds)
0
(_ (coerce seconds 'double-float)
random
(* 1000000000 _)
floor)))
(defun run (time-string &key (output-format :iso-8601) (jitter 0) timezone)
(local-time:format-timestring
*standard-output*
(local-time:timestamp+ (chronicity:parse time-string :endian-preference :middle)
(jitter-ns jitter)
:nsec)
:format (or (assocdr output-format *time-formats*)
(error "Unknown time format ~S." output-format))
:timezone (or timezone local-time:*default-timezone*)))
;;;; User Interface -----------------------------------------------------------
(defparameter *examples*
'(("Print the current time in RFC 3339 format:" . "when now --rfc-3339")
("Print a randomish time from a while ago:" . "when '12 hours ago' --jitter-hours 4")
("Print a randomish time next year in UTC:" . "when '1 year from now' --jitter-days 30 --utc")))
(defparameter *option/help*
(adopt:make-option 'help
:help "Display help and exit."
:long "help"
:short #\h
:reduce (constantly t)))
(defparameter *option/output-format/iso-8601*
(adopt:make-option 'output-format/iso-8601
:result-key 'output-format
:help "Output results as ISO-8601 (default)."
:long "iso-8601"
:short #\i
:initial-value :iso-8601
:reduce (constantly :iso-8601)))
(defparameter *option/output-format/iso-8601-seconds*
(adopt:make-option 'output-format/iso-8601-seconds
:result-key 'output-format
:help "Output results as ISO-8601 with a precision of seconds (no nanoseconds)."
:long "iso-8601-seconds"
:short #\I
:reduce (constantly :iso-8601-seconds)))
(defparameter *option/output-format/rfc-3339*
(adopt:make-option 'output-format/rfc-3339
:result-key 'output-format
:help "Output results as RFC 3339."
:long "rfc-3339"
:short #\r
:reduce (constantly :rfc-3339)))
(defparameter *option/output-format/gnuplot*
(adopt:make-option 'output-format/gnuplot
:result-key 'output-format
:help "Output results as Gnuplot timestamps."
:long "gnuplot"
:short #\g
:reduce (constantly :gnuplot)))
(defparameter *option/jitter/seconds*
(adopt:make-option 'jitter/seconds
:result-key 'jitter
:parameter "N"
:help "Jitter result by up to N seconds."
:long "jitter-seconds"
:short #\S
:initial-value 0
:key #'parse-integer
:reduce (lambda (old n) (declare (ignore old)) n)))
(defparameter *option/jitter/minutes*
(adopt:make-option 'jitter/minutes
:result-key 'jitter
:parameter "N"
:help "Jitter result by up to N minutes."
:long "jitter-minutes"
:short #\M
:key #'parse-integer
:reduce (lambda (old n) (declare (ignore old)) (* 60 n))))
(defparameter *option/jitter/hours*
(adopt:make-option 'jitter/hours
:result-key 'jitter
:parameter "N"
:help "Jitter result by up to N hours."
:long "jitter-hours"
:short #\H
:key #'parse-integer
:reduce (lambda (old n) (declare (ignore old)) (* 60 60 n))))
(defparameter *option/jitter/days*
(adopt:make-option 'jitter/days
:result-key 'jitter
:parameter "N"
:help "Jitter result by up to N days."
:long "jitter-days"
:short #\D
:key #'parse-integer
:reduce (lambda (old n) (declare (ignore old)) (* 24 60 60 n))))
(defparameter *option/timezone/local*
(adopt:make-option 'jitter/timezone/local
:result-key 'timezone
:help "Print result in current local computer timezone (default)."
:long "local-timezone"
:short #\l
:initial-value nil
:reduce (constantly nil)))
(defparameter *option/timezone/utc*
(adopt:make-option 'jitter/timezone/utc*
:result-key 'timezone
:help "Print the result in UTC."
:long "utc"
:short #\u
:reduce (constantly local-time:+utc-zone+)))
(adopt:define-string *help-text*
"when takes a human-readable time like '2 hours ago' and prints it as a ~
computer-readable time. It can also jitter the time by a random amount if ~
you want to produce randomish data.")
(defparameter *ui*
(adopt:make-interface
:name "when"
:usage "[OPTIONS] TIME-STRING"
:summary "convert human time to computer time"
:help *help-text*
:examples *examples*
:contents (list *option/help*
(adopt:make-group
'output-options
:title "Output Options"
:options (list *option/output-format/iso-8601*
*option/output-format/iso-8601-seconds*
*option/output-format/rfc-3339*
*option/output-format/gnuplot*))
(adopt:make-group
'timezone-options
:title "Timezone Options"
:options (list *option/timezone/local*
*option/timezone/utc*))
(adopt:make-group
'jitter-options
:title "Jitter Options"
:options (list *option/jitter/seconds*
*option/jitter/minutes*
*option/jitter/hours*
*option/jitter/days*)))))
(defun toplevel ()
(sb-ext:disable-debugger)
(setf *random-state* (make-random-state t))
(multiple-value-bind (arguments options) (adopt:parse-options-or-exit *ui*)
(handler-case
(adopt::quit-on-ctrl-c ()
(cond
((gethash 'help options) (adopt:print-help-and-exit *ui*))
((/= (length arguments) 1) (error 'need-one-time-string))
(t (run (first arguments)
:output-format (gethash 'output-format options)
:jitter (gethash 'jitter options)
:timezone (gethash 'timezone options)))))
(user-error (e) (adopt:print-error-and-exit e)))))