Compare commits
9 Commits
dev
...
format-dur
Author | SHA1 | Date |
---|---|---|
contrapunctus | c6301107cd | |
contrapunctus | 23b0376161 | |
contrapunctus | f51ccf7333 | |
contrapunctus | 9b69bbec8e | |
contrapunctus | 98a5cb2d41 | |
contrapunctus | 17bd1f53f1 | |
contrapunctus | 355e15a4ff | |
contrapunctus | 8eebc966d7 | |
contrapunctus | 16c9601a91 |
91
TODO.org
91
TODO.org
|
@ -486,49 +486,76 @@ Macro creates -
|
|||
|
||||
* unified format-duration function :code:customization:
|
||||
<2021-06-08T11:17:54+0530>
|
||||
Currently we have at least three ways of displaying durations - ="HH:MM:SS"= , ="XhYm"= , and =X hour(s), Y minutes(s)"= . Make a single function similar to =format-time-string=, but for durations. =ts-human-format-duration= from =ts.el= is not nearly as flexible as I'd like. When completed, we can have a single custom variable accepting a format string, which can be used to customize display of durations for the entire application with a uniform interface.
|
||||
|
||||
Currently we have at least three ways of displaying durations - ="HH:MM:SS"= , ="XhYm"= , and =X hour(s), Y minutes(s)"= . Make a single function similar to =format-time-string=, but for durations. =ts-human-format-duration= from =ts.el= is not nearly as flexible as I'd like. When completed, we can have a single custom variable accepting a format string, which can be used to customize display of durations for the entire application at once.
|
||||
** spec
|
||||
User provides a format string and a duration (in seconds).
|
||||
+ /format string/ is a mix of /unit specifiers/ and /unit separators/
|
||||
+ a /unit specifier/ is ="<escape character>[<zero value><value separator>]<code>"=
|
||||
* escape character can be either
|
||||
- =~= - no unit strings, e.g.
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(format-duration "~ss" 1) ;; => "1s"
|
||||
#+END_SRC
|
||||
or
|
||||
- =%= long unit strings, e.g. "minute". If the value is >1, plurals are used.
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(format-duration "%s" 1) ;; => "1 second"
|
||||
(format-duration "%s" 5) ;; => "5 seconds"
|
||||
#+END_SRC
|
||||
* zero value - what to do when the unit has a value of 0
|
||||
+ with no unit strings
|
||||
- if omitted, print =""= and omit the following unit separator, i.e. any text in the format string until the next escape character is encountered
|
||||
- if =0=, print ="0"=
|
||||
+ with long unit strings
|
||||
- if omitted, print =""= for a unit which has a value of 0
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(format-duration "%m %s" 5) ;; => "5 seconds"
|
||||
(format-duration "%m %s" 60) ;; => "1 minute"
|
||||
(format-duration "%m %s" 125) ;; => "2 minutes 5 seconds"
|
||||
#+END_SRC
|
||||
- if =0=, print ="0 <units>"=, e.g. ="0 years, 3 months"=
|
||||
* value separator is
|
||||
- used between value and unit, i.e. only applicable with =%= escape character
|
||||
- if unspecified, it is a space (?)
|
||||
* codes -
|
||||
- large units are capitals - =Y= (years), =M= (months), =W= (weeks), =D= (days)
|
||||
- small units are lower-case - =h= (hours), =m= (minutes), =s= (seconds)
|
||||
+ a /unit separator/ is any text between two unit specifiers.
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(format-duration "%m, %s" 61) ;; => "1 minute, 1 second"
|
||||
#+END_SRC
|
||||
|
||||
+ user provides a duration (in seconds), a format string, and an optional separator string
|
||||
+ ="%y"= , ="%o"= , ="%w"= , ="%d"= , ="%h"= , ="%m"= , ="%s"= - years (365 days), months (30 days? 4 weeks?), weeks, days, hours, minutes, seconds
|
||||
+ ="%Y"= , ="%O"= , ="%W"= , ="%D"= , ="%H"= , ="%M"= , ="%S"= - same as above, but with string units, e.g. ="hour(s)"= , ="minute(s)"= , and ="second(s)"=
|
||||
- if the unit is >1, plurals are used
|
||||
- separator can be specified like this - ="%<SEP><CODE>"= , e.g. ="%-T"=; only entered if there is a value present to the left; if unspecified, it is a space
|
||||
+ if the value is 0, the value and the unit are ignored even if provided in the input string
|
||||
+ optional separator string is interspersed between each value
|
||||
Special cases
|
||||
1. Non-continuous range of units, e.g. specifying seconds and days
|
||||
|
||||
Examples
|
||||
+ =(chronometrist-format-duration "%ss" 1)= => ="1s"=
|
||||
+ =(chronometrist-format-duration "%S" 1)= => ="1 second"=
|
||||
+ =(chronometrist-format-duration "%S" 5)= => ="5 seconds"=
|
||||
+ =(chronometrist-format-duration "%M %S" 5)= => ="5 seconds"=
|
||||
+ =(chronometrist-format-duration "%M %S" 60)= => ="1 minute"=
|
||||
+ =(chronometrist-format-duration "%M %S" 125)= => ="2 minutes, 5 seconds"=
|
||||
+ [[https://en.wikipedia.org/wiki/ISO_8601#Durations][ISO-8601]] -
|
||||
#+BEGIN_SRC emacs-lisp :tangle no :load no
|
||||
(chronometrist-format-duration "P%yY%oM%wW%dDT%hH%mM%sS"
|
||||
(+ (* 365 24 3600)
|
||||
(* 30 24 3600)
|
||||
(* 7 24 3600)
|
||||
(* 24 3600)
|
||||
(* 10 3600)
|
||||
(* 10 60)
|
||||
10)) ;; => "P1Y1M1W1DT10H10M10S"
|
||||
A simpler but less flexible solution would be to ditch the format string and let the user specify -
|
||||
1. duration,
|
||||
2. smallest and largest desired units,
|
||||
3. separator string,
|
||||
4. type of unit strings - long, short, or none,
|
||||
5. whether to use ISO format
|
||||
|
||||
** Examples
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
;; ISO duration format
|
||||
(chronometrist-format-duration "P~YY~MM~WW~DDT~hH~mM~sS"
|
||||
(+ (* 365 24 3600)
|
||||
(* 30 24 3600)
|
||||
(* 7 24 3600)
|
||||
(* 24 3600)
|
||||
(* 10 3600)
|
||||
(* 10 60)
|
||||
10)) ;; => "P1Y1M1W1DT10H10M10S"
|
||||
#+END_SRC
|
||||
|
||||
Alternative syntax
|
||||
+ large units are capitals - ="Y"= , ="M"= , ="W"= , ="D"=
|
||||
+ small units are lower-case - ="h"= , ="m"=, ="s"=
|
||||
+ to display only values, use ="%<code>"=
|
||||
+ to display long units, use ="~[<separator>]<code>"=
|
||||
|
||||
* DONE error - =min= called with nil :spark:bug:
|
||||
<2021-06-11T03:44:17+0530>
|
||||
1. clock in
|
||||
2. change =:start= of active interval to another time on the same date
|
||||
3. error
|
||||
|
||||
#+BEGIN_SRC emacs-lisp :tangle no :load no
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
Debugger entered--Lisp error: (wrong-number-of-arguments #<subr min> 0)
|
||||
min()
|
||||
apply(min nil)
|
||||
|
|
|
@ -955,6 +955,53 @@ SECONDS is less than 60, return a blank string."
|
|||
hours hour-string
|
||||
minutes minute-string)))))
|
||||
|
||||
(defun chronometrist-format-duration-helper (seconds largest-unit)
|
||||
"Return a duration of SECONDS as a list.
|
||||
Return value is (YEARS MONTHS WEEKS DAYS HOURS MINUTES SECONDS).
|
||||
|
||||
If SECONDS is zero, return nil.
|
||||
|
||||
A month is assumed to be 30 days, and a year 365 days."
|
||||
(if (zerop seconds)
|
||||
nil
|
||||
(let* ((sec (% seconds 60))
|
||||
(minutes (% (/ seconds 60) 60))
|
||||
(hours (% (/ seconds 60 60) 24))
|
||||
(days (% (/ seconds 60 60 24) 7))
|
||||
(weeks (% (/ seconds 60 60 24 7) 4))
|
||||
(months (% (/ seconds 60 60 24 7 4) 12))
|
||||
(years (/ seconds 365 24 3600)))
|
||||
(list years months weeks days hours minutes sec))))
|
||||
|
||||
(defvar chronometrist--format-duration-code-alist
|
||||
`(("Y" "year" ,(* 365 24 60 60))
|
||||
("M" "month" ,(* 30 24 60 60))
|
||||
("W" "week" ,(* 7 24 60 60))
|
||||
("D" "day" ,(* 24 60 60))
|
||||
("h" "hour" ,(* 60 60))
|
||||
("m" "minute" 60)
|
||||
("s" "second" 1))
|
||||
"Plist of duration units and their relation to seconds.")
|
||||
|
||||
(defun chronometrist-format-duration (format-string seconds)
|
||||
(cl-loop with case-fold-search = nil
|
||||
;; look for unit specifiers in format-string and replace them with
|
||||
;; duration values and (maybe) unit strings
|
||||
for (code unit divisor) in chronometrist--format-duration-code-alist do
|
||||
(when-let* ((code-regex (rx-to-string
|
||||
`(and (group-n 1 (or "~" "%")) ,code) t))
|
||||
(match-p (string-match code-regex format-string))
|
||||
(esc-char (match-string 1 format-string))
|
||||
(quotient (/ seconds divisor))
|
||||
(unit (if (= 1 quotient) unit (concat unit "s")))
|
||||
(replace (pcase esc-char
|
||||
("~" (format "%s" quotient))
|
||||
("%" (format "%s %s" quotient unit)))))
|
||||
(setq format-string (replace-regexp-in-string
|
||||
code-regex replace format-string t)
|
||||
seconds (if (zerop quotient) 0 (% seconds divisor))))
|
||||
finally return format-string))
|
||||
|
||||
(defcustom chronometrist-update-interval 5
|
||||
"How often the `chronometrist' buffer should be updated, in seconds.
|
||||
|
||||
|
|
|
@ -425,7 +425,7 @@ file.")
|
|||
"Return the name of the currently clocked-in task, or nil if not clocked in."
|
||||
(chronometrist-sexp-current-task))
|
||||
#+END_SRC
|
||||
*** format-time :function:
|
||||
*** format-duration :function:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(cl-defun chronometrist-format-duration (seconds &optional (blank (make-string 3 ?\s)))
|
||||
"Format SECONDS as a string suitable for display in Chronometrist buffers.
|
||||
|
@ -1920,6 +1920,168 @@ SECONDS is less than 60, return a blank string."
|
|||
(should (equal (chronometrist-format-duration-long 7260) "2 hours, 1 minute"))
|
||||
(should (equal (chronometrist-format-duration-long 7320) "2 hours, 2 minutes")))
|
||||
#+END_SRC
|
||||
*** format-duration-helper :function:
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun chronometrist-format-duration-helper (seconds largest-unit)
|
||||
"Return a duration of SECONDS as a list.
|
||||
Return value is (YEARS MONTHS WEEKS DAYS HOURS MINUTES SECONDS).
|
||||
|
||||
If SECONDS is zero, return nil.
|
||||
|
||||
A month is assumed to be 30 days, and a year 365 days."
|
||||
(if (zerop seconds)
|
||||
nil
|
||||
(let* ((sec (% seconds 60))
|
||||
(minutes (% (/ seconds 60) 60))
|
||||
(hours (% (/ seconds 60 60) 24))
|
||||
(days (% (/ seconds 60 60 24) 7))
|
||||
(weeks (% (/ seconds 60 60 24 7) 4))
|
||||
(months (% (/ seconds 60 60 24 7 4) 12))
|
||||
(years (/ seconds 365 24 3600)))
|
||||
(list years months weeks days hours minutes sec))))
|
||||
|
||||
#+END_SRC
|
||||
**** tests
|
||||
#+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test
|
||||
(ert-deftest chronometrist-format-duration-helper ()
|
||||
(should (null (chronometrist-format-duration-helper 0)))
|
||||
(should
|
||||
(equal (make-list 7 1)
|
||||
(chronometrist-format-duration-helper (+ (* 12 4 7 24 60 60)
|
||||
(* 4 7 24 60 60)
|
||||
(* 7 24 60 60)
|
||||
(* 24 60 60)
|
||||
(* 60 60)
|
||||
60 1))))
|
||||
(should
|
||||
(equal (make-list 7 2)
|
||||
(chronometrist-format-duration-helper (+ (* 2 12 4 7 24 60 60)
|
||||
(* 2 4 7 24 60 60)
|
||||
(* 2 7 24 60 60)
|
||||
(* 2 24 60 60)
|
||||
(* 2 60 60)
|
||||
(* 2 60) 2)))))
|
||||
#+END_SRC
|
||||
*** format-duration :function:
|
||||
1. In format-string, search for each unit from largest to smallest.
|
||||
2. When a unit is found, replace all occurrences of it in the format string with the duration. Set the input seconds to the remainder.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defvar chronometrist--format-duration-code-alist
|
||||
`(("Y" "year" ,(* 365 24 60 60))
|
||||
("M" "month" ,(* 30 24 60 60))
|
||||
("W" "week" ,(* 7 24 60 60))
|
||||
("D" "day" ,(* 24 60 60))
|
||||
("h" "hour" ,(* 60 60))
|
||||
("m" "minute" 60)
|
||||
("s" "second" 1))
|
||||
"Plist of duration units and their relation to seconds.")
|
||||
|
||||
(defun chronometrist-format-duration (format-string seconds)
|
||||
(cl-loop with case-fold-search = nil
|
||||
;; look for unit specifiers in format-string and replace them with
|
||||
;; duration values and (maybe) unit strings
|
||||
for (code unit divisor) in chronometrist--format-duration-code-alist do
|
||||
(when-let* ((code-regex (rx-to-string
|
||||
`(and (group-n 1 (or "~" "%")) ,code) t))
|
||||
(match-p (string-match code-regex format-string))
|
||||
(esc-char (match-string 1 format-string))
|
||||
(quotient (/ seconds divisor))
|
||||
(unit (if (= 1 quotient) unit (concat unit "s")))
|
||||
(replace (pcase esc-char
|
||||
("~" (format "%s" quotient))
|
||||
("%" (format "%s %s" quotient unit)))))
|
||||
(setq format-string (replace-regexp-in-string
|
||||
code-regex replace format-string t)
|
||||
seconds (if (zerop quotient) 0 (% seconds divisor))))
|
||||
finally return format-string))
|
||||
|
||||
#+END_SRC
|
||||
**** tests
|
||||
*****
|
||||
#+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test
|
||||
(ert-deftest chronometrist-format-duration ()
|
||||
(let* ((one-hour (* 60 60))
|
||||
(one-day (* 24 one-hour))
|
||||
(one-week (* one-day 7))
|
||||
(one-month (* one-day 30))
|
||||
(one-year (* one-day 365)))
|
||||
(should (equal (chronometrist-format-duration "~Y" 0) "0"))
|
||||
(should (equal (chronometrist-format-duration "~M" 0) "0"))
|
||||
(should (equal (chronometrist-format-duration "~W" 0) "0"))
|
||||
(should (equal (chronometrist-format-duration "~D" 0) "0"))
|
||||
(should (equal (chronometrist-format-duration "~h" 0) "0"))
|
||||
(should (equal (chronometrist-format-duration "~m" 0) "0"))
|
||||
(should (equal (chronometrist-format-duration "~s" 0) "0"))
|
||||
|
||||
(should (equal (chronometrist-format-duration "%Y" 0) "0 years"))
|
||||
(should (equal (chronometrist-format-duration "%M" 0) "0 months"))
|
||||
(should (equal (chronometrist-format-duration "%W" 0) "0 weeks"))
|
||||
(should (equal (chronometrist-format-duration "%D" 0) "0 days"))
|
||||
(should (equal (chronometrist-format-duration "%h" 0) "0 hours"))
|
||||
(should (equal (chronometrist-format-duration "%m" 0) "0 minutes"))
|
||||
(should (equal (chronometrist-format-duration "%s" 0) "0 seconds"))
|
||||
|
||||
(should (equal (chronometrist-format-duration "%Y" 1) "0 years"))
|
||||
(should (equal (chronometrist-format-duration "%M" 1) "0 months"))
|
||||
(should (equal (chronometrist-format-duration "%W" 1) "0 weeks"))
|
||||
(should (equal (chronometrist-format-duration "%D" 1) "0 days"))
|
||||
(should (equal (chronometrist-format-duration "%h" 1) "0 hours"))
|
||||
(should (equal (chronometrist-format-duration "%m" 1) "0 minutes"))
|
||||
(should (equal (chronometrist-format-duration "%s" 1) "1 second"))
|
||||
|
||||
(should (equal (chronometrist-format-duration "%Y" 60) "0 years"))
|
||||
(should (equal (chronometrist-format-duration "%M" 60) "0 months"))
|
||||
(should (equal (chronometrist-format-duration "%W" 60) "0 weeks"))
|
||||
(should (equal (chronometrist-format-duration "%D" 60) "0 days"))
|
||||
(should (equal (chronometrist-format-duration "%h" 60) "0 hours"))
|
||||
(should (equal (chronometrist-format-duration "%m" 60) "1 minute"))
|
||||
(should (equal (chronometrist-format-duration "%s" 60) "60 seconds"))
|
||||
|
||||
(should (equal (chronometrist-format-duration "%Y" (* 60 60)) "0 years"))
|
||||
(should (equal (chronometrist-format-duration "%M" (* 60 60)) "0 months"))
|
||||
(should (equal (chronometrist-format-duration "%W" (* 60 60)) "0 weeks"))
|
||||
(should (equal (chronometrist-format-duration "%D" (* 60 60)) "0 days"))
|
||||
(should (equal (chronometrist-format-duration "%h" (* 60 60)) "1 hour"))
|
||||
(should (equal (chronometrist-format-duration "%m" (* 60 60)) "60 minutes"))
|
||||
(should (equal (chronometrist-format-duration "%s" (* 60 60)) "3600 seconds"))
|
||||
|
||||
(should (equal (chronometrist-format-duration "%Y" one-day) "0 years"))
|
||||
(should (equal (chronometrist-format-duration "%M" one-day) "0 months"))
|
||||
(should (equal (chronometrist-format-duration "%W" one-day) "0 weeks"))
|
||||
(should (equal (chronometrist-format-duration "%D" one-day) "1 day"))
|
||||
(should (equal (chronometrist-format-duration "%h" one-day) "24 hours"))
|
||||
(should (equal (chronometrist-format-duration "%m" one-day) "1440 minutes"))
|
||||
(should (equal (chronometrist-format-duration "%s" one-day) "86400 seconds"))
|
||||
|
||||
(should (equal (chronometrist-format-duration "%Y" one-week) "0 years"))
|
||||
(should (equal (chronometrist-format-duration "%M" one-week) "0 months"))
|
||||
(should (equal (chronometrist-format-duration "%W" one-week) "1 week"))
|
||||
(should (equal (chronometrist-format-duration "%D" one-week) "7 days"))
|
||||
(should (equal (chronometrist-format-duration "%h" one-week) "168 hours"))
|
||||
(should (equal (chronometrist-format-duration "%m" one-week) "10080 minutes"))
|
||||
(should (equal (chronometrist-format-duration "%s" one-week) "604800 seconds"))
|
||||
|
||||
;; multiple unit specifiers
|
||||
|
||||
;; ISO format - all units, no unit strings
|
||||
(should
|
||||
(equal "P1Y1M1W1DT10H10M10S"
|
||||
(chronometrist-format-duration "P~YY~MM~WW~DDT~hH~mM~sS"
|
||||
(+ one-year one-month
|
||||
one-week one-day
|
||||
(* 10 one-hour)
|
||||
(* 10 60) 10))))
|
||||
|
||||
;; all units with unit strings
|
||||
(should
|
||||
(equal (chronometrist-format-duration "%Y, %M, %W, %D, %h, %m, and %s"
|
||||
(+ one-year one-month
|
||||
one-week one-day
|
||||
(* 10 one-hour)
|
||||
(* 10 60) 10))
|
||||
"1 year, 1 month, 1 week, 1 day, 10 hours, 10 minutes, and 10 seconds"))))
|
||||
#+END_SRC
|
||||
** Timer
|
||||
Instead of the Emacs convention of pressing ~g~ to update, we keep buffers updated with a timer.
|
||||
|
||||
|
|
Reference in New Issue