Compare commits

...
This repository has been archived on 2022-05-13. You can view files and clone it, but cannot push or open issues or pull requests.

9 Commits

Author SHA1 Message Date
contrapunctus c6301107cd doc(TODO): remove source block header args 2021-07-08 14:38:02 +05:30
contrapunctus 23b0376161 doc(TODO): remove separator parameter 2021-07-08 14:37:41 +05:30
contrapunctus f51ccf7333 fix: incorrect seconds value for month 2021-07-08 14:12:09 +05:30
contrapunctus 9b69bbec8e feat: implement handling of ~ and % 2021-07-08 06:54:57 +05:30
contrapunctus 98a5cb2d41 doc(TODO): swap meaning of % and ~; add zero value field 2021-07-08 03:12:13 +05:30
contrapunctus 17bd1f53f1 feat: (WIP) define format-duration function 2021-07-08 01:53:56 +05:30
contrapunctus 355e15a4ff doc(TODO): move examples into spec 2021-07-07 23:21:29 +05:30
contrapunctus 8eebc966d7 doc(TODO): remove old spec, elaborate on newer idea 2021-07-07 22:01:38 +05:30
contrapunctus 16c9601a91 feat: create format-duration-helper 2021-07-07 21:59:32 +05:30
3 changed files with 269 additions and 33 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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.