Emacs Hack - 通过列表数据创建表格

1 问题描述

OrgMode内置的创建表格的函数是 org-table-create , 传入"列数x行数"参数即可生成特定行数、列数的表格。这种交互函数在编写org文档时很实用,但在代码中却显得鸡肋。因为在代码中,我们通常希望表格和数据可以一起生成,而不是手动添加数据。

我在折腾 gk-habit.el 时,就产生了这样的需求:生成习惯的月度打卡视图。就像下面这个样子:

Sun Mon Tue Wed Thu Fri Sat
            1
            --
2 3 4 5 6 7 8
-- -- -- -- -- --
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31          
         

2 思路分析

解决这个问题的关键是把握几个操作org表格的函数 org-create-table, org-table-next-field, org-table-insert-hline, org-table-kill-row… 然后就是在表格创建的过程中依次插入数据。用于创建表格的每一行的数据用列表表示,分隔线用 hl 对象表示。

  • 首先创建一个一行n列的表格,因为 org table 的函数只有在表格内才能使用。其中n为每行元素的个数。
  • 在数据列表中循环,如果元素是一个list,表示是数据。继续在该list中循环,插入数据后跳到下一个单元格(注意数字要转为字符串)。
  • 如果元素是 hl 对象,表示是分隔线,直接插入一行分割线 (org-table-insert-hline 1)
  • 每一行插入最后一个数据后会执行“跳到下一个单元格”的操作,当右边没有单元格时会自动插入新的一行。
  • 因此,最后会多出一行,用 org-table-kill-row 函数删掉。

3 代码实现

(defun gk-org-table-create (LIST)
  "Create org table from a LIST form at point."
  (let ((column (catch 'break
                  (dolist (row-data LIST)
                    (when (listp row-data)
                      (throw 'break (length row-data))))))
        (beg (point)))
    (org-table-create (concat (number-to-string column) "x1"))
    (goto-char beg)
    (when (org-at-table-p)
      (org-table-next-field)
      (dotimes (i (length LIST))
        (let ((row-data (nth i LIST)))
          (if (listp row-data)
              (dolist (data row-data)
                (cond
                 ((numberp data)
                  (insert (number-to-string data)))
                 ((null data)
                  (insert ""))
                 (t (insert data)))
                (org-table-next-field))
            (when (equal 'hl row-data)
              (org-table-insert-hline 1)))
          (when (= i (1- (length LIST)))
            (org-table-kill-row))))))
  (forward-line))

4 使用案例

(gk-org-table-create
 '(("n1" "n2" "n3" "n4" "n5")
   hl
   (1 2 3 4 5)
   (6 7 8 9 10)
   hl
   ("c1" "c2" "c3" "c4" "c5")
   hl
   ("a" "b" "c" "d" "e")
   ("f" "g" "h" "i" "j")))
| n1 | n2 | n3 | n4 | n5 |
|----+----+----+----+----|
| 1  | 2  | 3  | 4  | 5  |
| 6  | 7  | 8  | 9  | 10 |
|----+----+----+----+----|
| c1 | c2 | c3 | c4 | c5 |
|----+----+----+----+----|
| a  | b  | c  | d  | e  |
| f  | g  | h  | i  | j  |

实现开篇提出的习惯打卡的视图是个更复杂的问题,这里涉及到了不同月份的天数不同,起始星期不同,以及每天对应的打卡状态不同等问题。解决了这些问题后,将得到的数据整合成 gk-org-table-create 合法的数据列表形式即可生成相应的表格。相关代码在这里

如果你有更简单、漂亮的实现,欢迎留言探讨~

发布于 2020-08-19

Emacs Hack - 分割文件相似内容

实际问题

将一个文件中的相似块分割成若干个文件。比如下面这个例子:

第一回: 灵根育孕源流出 心性修持大道生
  这是第一段内容.....
  这是第二段内容.....
  这是第三段内容.....
  更多...

第二回: 悟彻菩提真妙理 断魔归本合元神
  这是第一段内容.....
  这是第二段内容.....
  这是第三段内容.....
  更多...

第三回: 四海千山皆拱伏 九幽十类尽除名
  这是第一段内容.....
  这是第二段内容.....
  这是第三段内容.....
  更多...

假设有一个长篇小说txt文件,每一回有相似的结构。要解决的问题是:如何将每一回分割成单独的文件?文件名是每一回的标题,文件的内容就是每一回的内容。

解决思路

解决这种实际的问题,我的思路一般是:用人的思维找到直观的解决办法。然后,将人的思维转换成对应的代码逻辑,比如:运用什么控制语句?使用什么数据结构?涉及什么特殊算法?等等。

首先我们从人的思维出发考虑如何解决这个问题。最直接的方法是:根据开头“第一回:”这几个字确定这一回的标题,然后确定接下来直到“第二回:”之间的部分为第一回的内容。然后将第一回的内容写到以标题命名的文件中。接下里,以此类推,直到最后没有内容。

接下来,思考如何将这个过程转化成代码的逻辑。

显然,这里是循环判断的逻辑,循环的范围是从第一行到最后一行。分析过程如下:

  1. 搜索第一行,是标题行,记录标题并移到下一行。
  2. 搜索这一行,不是标题行,记录行首的位置start。判断下一行是否是标题行,不是则移到下一行。
  3. 重复过程2,直到当前行的下一行是标题行时,记录本行行首的位置end。
  4. 将start和end之间的内容写到以标题命名的文件中。移到下一行。
  5. 重复上面的过程1,2,3,4。直到光标到达最后。

代码实现

根据上面的分析过程,有如下的代码:

(defun my-split-file (file)
  (interactive "fchoose a file: ")
  (let ((filedir "~/test-dir/")
        filename start end content)
    (with-current-buffer (get-buffer-create "*split-file*")
      (insert-file-contents file)
      (goto-char (point-min))
      (while (< (point) (point-max))
        (if (search-forward-regexp "^第.+回:" (line-end-position) t)
            (progn 
              (setq filename (string-trim (thing-at-point 'line))) 
              (next-line)
              (beginning-of-line)
              (setq start (point))) ;; 获取内容的起始位置start
          (save-excursion ;; 保存光标的位置:只判断下一行的情况,不改变实际要操作的光标。
            (next-line)
            (beginning-of-line)
            (when (or (search-forward-regexp "^第.+回:" (line-end-position) t)
                      (= (point) (point-max)))
              (previous-line)
              (end-of-line)
              (setq end (point)) ;; 获取内容的结尾位置end
              (setq content (buffer-substring-no-properties start end)) ;; 获取start和end之间的内容。
              (with-temp-file (concat filedir filename ".txt")
                (insert content)) ;; 写文件
              ))
          (next-line)
          (beginning-of-line))))))
发布于 2020-06-28