1

I have files such as these:

0000
0030
+ Something
+ Another thing
+ One more thing
0200
+ Something else
+ And one more thing
0230

In the above, the numbers represent time in HHMM format. I want to transform it to:

0010 + Something
0020 + Another thing
0030 + One more thing
0115 + Something else
0200 + And one more thing

I will explain the transformation in the above example:

  • between 0030 and 0200 there are 3 lines. And time difference between 0000 and 0030 is 30 minutes. So each item below 0030 gets a 10 minutes increment starting from 0000. And thus the 3 new timestamps are 0010, 0020 and 0030 respectively.

  • similarly between 0200 and 0230 there are 2 lines. And the time difference between 0030 and 0200 is 90 minutes. So each item below 0200 gets 45 minutes increment starting from 0030. So the 2 new timestamps are 0115 and 0200 respectively.

I have made some progress in writing an elisp-macro for this transformation task:

  1. I could modify the answer to this emacs.stackexchange post to compute the time difference between times.

  2. I could write a function that takes in time and minutes elapsed to print new time after adding elapsed minutes to time.

  3. Now the only thing that I need to complete my recipe is: the number of items between two consecutive HHMM patterns so that I can split the difference of time evenly over the items.

  4. Lastly I plan to wrap up all the above inside a (while (search-forward-regexp "pattern" nil :noerror) to apply the above macros to everywhere applicable inside the file.

Here is the summary of my failed attempts so far:

I recorded a keyboard macro to mark the region between two consecutive HHMM pattern and then I am trying to use (count-lines start end). However I get the error:

Symbol's value as a variable is void: start

Here is my code snippet for this attempt:

(defun timesheet-fill-times ()
  (interactive)
  (let (diff lines)
      (setq diff (timesheet-time-diff))
      (timesheet-mark-lines)
      (setq lines (count-lines start end))
      (message "%d diff %d lines" diff lines)
      )
    )

where timesheet-time-diff is my elisp macro to compute time difference between consecutive patterns and timesheet-mark-lines is my recorded keyboard macro to mark the relevant region. I tried to modify the function definition to: (defun timesheet-fill-times (start end) ;; rest of code ). Now the error becomes:

Wrong number of arguments

However, strangely, the following works alright: I have defined this function:

(defun my-count-lines-region (start end)
   (interactive "r")
   (message "%d"
       (count-lines start end)))

So if I keep my cursor on + after 0030 and do M-x timesheet-mark-lines RET M-x my-count-lines-region RET I get 3 as expected. And similarly with the cursor on + after 0200 I get 2 as expected.

My other approach I thought of was that: I can match for the pattern HHMM - some lines - HHMM by using something like this:

(re-search-forward (concat "^\\([0-2][0-9]\\)\\([0-5][0-9]\\)"
                            "\\(\\(.\\|[\n]\\)*?\\)"
                            "\\([0-2][0-9]\\)\\([0-5][0-9]\\)"
                            ) nil :noerror)

Is there someway I can count the number of newline characters in that match?

I have tried my best but I feel stuck. Please advice.

4
  • My guess is that it would be simpler to write this in python (or perl if you are so inclined).
    – NickD
    Jul 3, 2021 at 3:10
  • Sure, great suggestion. But, I am trying to learn lisp. And it feels so great when something works! Jul 3, 2021 at 4:33
  • You will not learn much lisp of this, but you could use count-words-region. Despite its name, it also gives you the number of lines is the region. Jul 3, 2021 at 5:17
  • 1
    You can get the match with match-string 0. You will find what exactly this pattern matches to (which does not include multiple lines). Otherwise you could split-string on \n and get the length of the resulting list. You can test your regexp with the regexp-builder. Jul 3, 2021 at 6:02

2 Answers 2

2

THIRD EDIT (added after @Inspired_Blue's answer)

@Inspired_Blue I did not try to completely follow and understand your answer. But as a matter of feedback, I will post here my way of solving the problem. While incidentally it inserts the correct times, you should still add functions to get the correct times for the general case.

I strongly recommend you to get familiar with edebug as it is only a really tiny bit of reading, while it will greatly boost your productivity. Then practice how to use edebug, while using it for reading my solution.

In short, in my solution I first parse the buffer into the following list (call se-answer interactively on your original text to see a pretty printed version of the list), where the car of each element is a pair (cons) with the start and end times, and the cdr forms a list containing the lines (the length of this list, effectively counts the number of lines between consecutive matches, providing an answer to your question).

(((0 . 30))
 ((30 . 200) "+ Something\n" "+ Another thing\n" "+ One more thing\n")
 ((200 . 230) "+ Something else\n" "+ And one more thing\n"))

Subsequently I format the list 'back' to your transformed text using a "list-eater" (term used in the books "Land of lisp" and "Realm of racket")

;; define handy macro for pretty printing
(defmacro parse-print (data &optional stream mode)
  "Return value but pretty print when called interactively.
Optionally set STREAM (see `pp'). If STREAM is a buffer then
optionally set MODE for buffer (for syntax highlight)."
  `(if (not (called-interactively-p))
       ,data
     (pp ,data ,stream)
     (when (bufferp ,stream))
     (pop-to-buffer ,stream)
     (when ,mode
       (funcall ,mode))))

(defun se-answer ()
  (interactive)
  ;; to prevent infinite loop first delete all trailing blank lines and
  ;; whitespace, and if exist final newline
  (delete-trailing-whitespace)
  (goto-char (point-max))
  (when (bolp)
    (delete-backward-char 1))
  (goto-char (point-min))
  ;; parse buffer
  (let (blocks) ;; we designate blocks for each part between a start- and end-time
    (while (not (eobp))
      ;; use only this while loops body for single block (e.g. for your keyboard
      ;; macro)
      (let (lines ;; per block we collect (push) the text lines in (to) a list
            (start-time (thing-at-point 'number))) ;; also we collect the start-time
        (forward-line)
        (while (not (thing-at-point 'number))
          (push (thing-at-point 'line t) lines)
          (forward-line))
        (push (cons (cons start-time (thing-at-point 'number))
                    (nreverse lines)) ;; and collect (push) the start- and end-time plus
              blocks)                 ;; the lines in the block in (to) the list of blocks
        (end-of-line)))
    (unless (called-interactively-p)
      (erase-buffer))
    ;; NOTE that you can call this function/command interactively to see the
    ;; pretty printed list.
    (parse-print (nreverse blocks) (get-buffer-create "parsed-list") 'emacs-lisp-mode)))

(defun insert-transformed-text (parsed-buffer)
  (interactive (list (se-answer)))
  ;; First an exit condition for. A list eater 'eats' the elements of the list
  ;; so we exit when there is only one element left (which can optionally get
  ;; inserted to the buffer).
  (if (not (cdr parsed-buffer))
      (if (y-or-n-p "Include final time?")
          (insert (format "%s" (cdaar parsed-buffer)))
        nil)
  (let* ((n (length (cdadr parsed-buffer)))
         (int-size (/ (- (cdaar parsed-buffer) (caaar parsed-buffer)) n)))
    (let ((time (+ (caaar parsed-buffer) int-size)))
      (dolist (x (cdadr parsed-buffer))
        (insert (format "%s %s" time x))
        (setq time (+ time int-size)))))
  ;; By passing back the 'tail' of the list, the function 'slowly eats' the
  ;; list.
  (insert-transformed-text (cdr parsed-buffer))))

Find a buffer with C-x C-f and give it some name ending with the .el (this opens a buffer in emacs-lisp-mode), paste and load the code in that buffer. Then open a scratch buffer in a split screen and paste your original text/list. Then instrumentalize the insert-transformed-text (or the se-answer) function/command (see edebug), and finally in the scratch buffer (with ONLY your original text) run the command M-x insert-transformed-text.

Have fun!

SECOND EDIT

I see now that you already found count-lines (I read over it). So the (interactive "r") provides your function with region-beginning and region-end as its arguments (see here and here).

I've added a comment to answer your question about lines in the regexp match. But I would strongly advise not to approach the problem in this way.

FIRST EDIT

Programmatically, count-words-region does not fulfil your needs directly. But by inspecting the function you will find the function count-words--message, from which you get another answer by inspecting its code (simpler than my provided solution).

END EDIT

As it seems you want to mark the region, I provided a simplest answer as a comment. Now I am not sure what you ask exactly, but more programmatically, I would propose something as follows:

(defun se-answer ()
  (interactive)
  (let ((start-time (thing-at-point 'number))
        (start-line (string-to-number (cadr (split-string (what-line))))))
    (forward-line) 
    (re-search-forward "^[0-9]+")
    (let ((end-time (thing-at-point 'number))
          (end-line (string-to-number (cadr (split-string (what-line))))))
      (message "start-time %s\nstart-line %s\nend-time   %s\nend-line   %s\nnum lines: %s"
               start-time
               start-line
               end-time
               end-line
               (- end-line start-line 1)))))
1

@dalanicolai's se-answer function (see here), I believe, is superior to my solution as mine uses regular expressions and hence is possibly slower. Hence I accept his solution as the answer to this question.

However I will share my solution here as well (for anyone who may learn something new from code snippets on stackexchange, like I do):

(defun my-count-occurences (regex string)
  (my-recursive-count regex string 0))

(defun my-recursive-count (regex string start)
  (if (string-match regex string start)
      (+ 1 (my-recursive-count regex string (match-end 0)))
    0))

(defun timesheet-count-items ()
  (interactive)
  (let (match diff lines)
    (save-excursion
      (if (re-search-forward (concat "^\\(\\(.\\|[\n]\\)*?\\)"
                                     "\\([0-2][0-9][0-5][0-9]\\)"
                     ) nil :noerror)
      (my-count-occurences "\n" (match-string 1))
    ))))

(defun timesheet-time-diff ()
  (interactive)
  (let (begin-hh begin-mm end-hh end-mm diff)
    (save-excursion
      (if (re-search-backward (concat "^\\([0-2][0-9]\\)\\([0-5][0-9]\\)"
                                      "\\(\\(.\\|[\n]\\)*?\\)"
                                      "\\([0-2][0-9]\\)\\([0-5][0-9]\\)"
                                      ) nil :noerror)
          (progn
            (setq begin-hh (string-to-number (match-string 1))) 
            (setq begin-mm (string-to-number (match-string 2))) 
            (setq end-hh (string-to-number (match-string 5))) 
            (setq end-mm (string-to-number (match-string 6)))
            (setq diff (- (+ end-mm  
                 (* 60 end-hh))
              (+ begin-mm 
                 (* 60 begin-hh))))
        (list begin-hh begin-mm diff)
        )
    )
      )))

(defun timesheet-time-add (hh mm de)
  (interactive)
  (let (M h m O)
    (save-excursion
      (progn
    (setq M (+ (* 60 hh) mm de))
    (setq h (/ M 60))
    (setq m (% M 60))
    (setq O (concat (format "%02d" h)(format "%02d" m)))
))))

(defun timesheet-fill-time ()
  (interactive)
  (let (temp count diff step start-hh start-mm delta)
    (save-excursion
      (setq count (timesheet-count-items))
      (setq temp (timesheet-time-diff))
      (setq start-hh (nth 0 temp)
        start-mm (nth 1 temp)
        diff (nth 2 temp))
      (setq step (/ diff count))
      (while (> count 0)
    (setq count (- count 1))
    (setq delta (- diff (* count step)))
    (beginning-of-line)
    (insert (concat (timesheet-time-add start-hh start-mm delta)
            " "))
    (next-line)
    )
      )))

(defun timesheet-process ()
  (interactive)
  (goto-char (point-min))
  (while (re-search-forward "
\\([^
0-9]\\)" nil t)
    (backward-char 1)
    (timesheet-fill-time))
  (goto-char (point-min))
  (while (search-forward-regexp "
\\([0-9]\\{4\\}\\)
" nil t) (replace-match "
" t nil))
  )

So to compare my solution with @dalanicolai's solution:

  1. I solve for the entire transformation as in the example of my question.
  2. Clearly @dalanicolai's solution is more elegant and pithy.
  3. I will have to adapt @dalanicolai's solution to my needs to carry out the entire transformation (which I plan to do sometime soon and add as an edit to my answer).
  4. My files have about 400-600 lines. And even my solution runs almost instantaneously.

Lastly, if you happen to read my code and are kind enough to offer me feedback, please do. As I have just started learning lisp, I will value such things immensely!

2
  • FYI, I have once more edited my answer Jul 3, 2021 at 11:16
  • Many thanks! Very helpful. Jul 3, 2021 at 13:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.