;; -*- lexical-binding: t; -*- ;;; nf-updver --- increment date-based version numbers ;; Author: Noah Friedman ;; Created: 2025-10-25 ;; Version: 2025.10.25.03 ;; Public domain ;;; Commentary: ;; This is somewhat janky at present. ;; Only ISO8601-style date order (YYYY-mm-dd, separators optional) are ;; understood. For the sake of sanity to everyone everywhere reading ;; timestamps, and that version numbers are always in descending order of ;; significance, please just don't use any other order. ;;; Code: (defgroup nf-updver nil "Increment date-based version number before saving buffer." :group editing :group convenience) ;;;###autoload (defcustom nf-updver-variable-name-regexp (format "\\(?:%s\\)" (mapconcat 'identity '("$?VERSION" "$?Version" "$?[Pp]rogram_?[Vv]ersion" ) "\\|")) "Regular expression matching the name of variable used to indicate version. This regexp is substituted into `nf-updver-version-regexp' wherever the substring \"%(VERSION)\" appears." :type 'regexp) ;; The nested concats are not necessary, but they help visualize ;; elements that are grouped together. ;;;###autoload (defcustom nf-updver-version-regexp (concat (concat "^\\(?:" (mapconcat 'identity `("#\\s-*define\\s-+%(VERSION)\\s-+" ;; C/C++ macro "\\s-*%(VERSION)\\s-*=\\s-*" ;; Python ,(concat "\\s-*\\(?:" ;; comment (mapconcat 'identity '("[#;]+" ;; script "//+" ;; C/C++ "/\\*") ;; C/C++ "\\|") "\\)" "\\s-*%(VERSION)\\s-*:\\s-*")) "\\|") "\\)") "\\(?1:f?['\"]\\|\\)?" ;; string delimiter (optional) (concat "\\(?2:" "[0-9]\\{4\\}" ;; YYYY "\\(?3:[/:,._-]*\\)" ;; separator (optional) "[0-9]\\{2\\}" ;; MM "\\3" ;; separator (optional) "[0-9]\\{2\\}" ;; DD "\\)") "\\(?:[/:,._-]*\\)" ;; separator (optional) "\\(?4:[0-9]\\{2,3\\}\\)" ;; II or III "\\1") ;; string delimiter (optional) "Regular expression matching line which sets program version number. This regexp should contain numbered groups matching the following elements of the assignment: \t1\tString delimiter around version number (e.g. \\=\", \\=', or possibly none) \t2\t4-digit year, 2-digit month, 2-digit day (YYYYmmdd) \t3\tSeparator between date elements (e.g. `.', \\+`-', or possibly none) \t4\tIteration count, a 2 or 3 digit number Examples of version string formats that might match are \t2025102501 \t20251025/02 \t2025.10.25.003 The string literal \"%(VERSION)\" is the placeholder for the value of `nf-updver-variable-name-regexp', which matches the name of the variable or prefix of the version definition and may be used more than once." :type 'regexp) (defun nf-updver-subst-version-regexp () (save-match-data (let ((re nf-updver-version-regexp) (vn nf-updver-variable-name-regexp)) (while (string-match "%(VERSION)" re) (setq re (replace-match vn t t re))) re))) (defun nf-updver-update-version () (interactive) (save-excursion (save-restriction (widen) (save-match-data (let ((case-fold-search nil) (version-regexp (nf-updver-subst-version-regexp))) (goto-char (point-min)) (when (re-search-forward version-regexp nil t) (let* ((sep (match-string 3)) (fmt (mapconcat 'identity '("%Y" "%m" "%d") sep)) (today (format-time-string fmt)) (bufdate (match-string 2)) (oldNN (match-string 4)) (newNN (make-string (length oldNN) ?0))) (cond ((string= today bufdate) (setq fmt (format "%%0%dd" (length oldNN))) (setq newNN (format fmt (1+ (string-to-number oldNN)))) (replace-match newNN t t nil 4)) (t (replace-match today t t nil 2) (replace-match newNN t t nil 4)))))))))) (defun nf-updver-update-before-save () (when nf-updver-mode (nf-updver-update-version)) ;; Always return nil for write-file-hooks. nil) ;;;###autoload (define-minor-mode nf-updver-mode "Increment the program version number before the file is saved." :lighter " UpdVer" (add-hook 'write-file-functions 'nf-updver-update-before-save)) (provide 'nf-updver) ;; local variables: ;; eval: (nf-updver-mode 1) ;; end: ;; nf-updver.el ends here