* Month-week and quarter-week datetrees (RFC and package announcement)
@ 2023-12-30 19:41 Jack Kamm
2023-12-31 14:50 ` Ihor Radchenko
0 siblings, 1 reply; 11+ messages in thread
From: Jack Kamm @ 2023-12-30 19:41 UTC (permalink / raw)
To: emacs-orgmode
I have started a package that defines org-capture datetrees following
year-quarter-week and year-month-week formats:
https://gitlab.com/jackkamm/org-grouped-weektree
I'd appreciate feedback on 2 points:
1. Are any of these datetree formats worth upstreaming into org-mode
proper?
2. Can we add a public interface for `org-datetree--find-create', and
are there any suggestions on how to do it?
Regarding #2, an ugly aspect of my current implementation is the abuse
of the private function `org-datetree--find-create'. I also pass in
the week or quarter for the DAY and MONTH arguments of this function,
though I note that `org-datetree-find-iso-week-create' does something
similar in its implementation.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2023-12-30 19:41 Month-week and quarter-week datetrees (RFC and package announcement) Jack Kamm
@ 2023-12-31 14:50 ` Ihor Radchenko
2023-12-31 18:16 ` Jack Kamm
0 siblings, 1 reply; 11+ messages in thread
From: Ihor Radchenko @ 2023-12-31 14:50 UTC (permalink / raw)
To: Jack Kamm; +Cc: emacs-orgmode
Jack Kamm <jackkamm@gmail.com> writes:
> https://gitlab.com/jackkamm/org-grouped-weektree
>
> I'd appreciate feedback on 2 points:
>
> 1. Are any of these datetree formats worth upstreaming into org-mode
> proper?
That would make sense.
> 2. Can we add a public interface for `org-datetree--find-create', and
> are there any suggestions on how to do it?
>
> Regarding #2, an ugly aspect of my current implementation is the abuse
> of the private function `org-datetree--find-create'. I also pass in
> the week or quarter for the DAY and MONTH arguments of this function,
> though I note that `org-datetree-find-iso-week-create' does something
> similar in its implementation.
The API of `org-datetree--find-create' is generally very limiting.
It would be nice to come up with something less limiting.
--
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2023-12-31 14:50 ` Ihor Radchenko
@ 2023-12-31 18:16 ` Jack Kamm
2024-12-16 18:49 ` Ihor Radchenko
0 siblings, 1 reply; 11+ messages in thread
From: Jack Kamm @ 2023-12-31 18:16 UTC (permalink / raw)
To: Ihor Radchenko; +Cc: emacs-orgmode
Ihor Radchenko <yantar92@posteo.net> writes:
>> https://gitlab.com/jackkamm/org-grouped-weektree
>>
>> I'd appreciate feedback on 2 points:
>>
>> 1. Are any of these datetree formats worth upstreaming into org-mode
>> proper?
>
> That would make sense.
>
>> 2. Can we add a public interface for `org-datetree--find-create', and
>> are there any suggestions on how to do it?
>>
>> Regarding #2, an ugly aspect of my current implementation is the abuse
>> of the private function `org-datetree--find-create'. I also pass in
>> the week or quarter for the DAY and MONTH arguments of this function,
>> though I note that `org-datetree-find-iso-week-create' does something
>> similar in its implementation.
>
> The API of `org-datetree--find-create' is generally very limiting.
> It would be nice to come up with something less limiting.
Thanks for the feedback -- I'll start working on something along these
lines. Though this might take me a little while since the holiday is
ending soon :''-(
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2023-12-31 18:16 ` Jack Kamm
@ 2024-12-16 18:49 ` Ihor Radchenko
2024-12-28 6:09 ` Jack Kamm
2024-12-29 9:18 ` Jack Kamm
0 siblings, 2 replies; 11+ messages in thread
From: Ihor Radchenko @ 2024-12-16 18:49 UTC (permalink / raw)
To: Jack Kamm; +Cc: emacs-orgmode
Jack Kamm <jackkamm@gmail.com> writes:
>> The API of `org-datetree--find-create' is generally very limiting.
>> It would be nice to come up with something less limiting.
>
> Thanks for the feedback -- I'll start working on something along these
> lines. Though this might take me a little while since the holiday is
> ending soon :''-(
Maybe the holiday is just beginning this year? Bumping this thread just
in case ;)
--
Ihor Radchenko // yantar92,
Org mode maintainer,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2024-12-16 18:49 ` Ihor Radchenko
@ 2024-12-28 6:09 ` Jack Kamm
2024-12-29 9:18 ` Jack Kamm
1 sibling, 0 replies; 11+ messages in thread
From: Jack Kamm @ 2024-12-28 6:09 UTC (permalink / raw)
To: Ihor Radchenko; +Cc: emacs-orgmode
Ihor Radchenko <yantar92@posteo.net> writes:
> Jack Kamm <jackkamm@gmail.com> writes:
>
>>> The API of `org-datetree--find-create' is generally very limiting.
>>> It would be nice to come up with something less limiting.
>>
>> Thanks for the feedback -- I'll start working on something along these
>> lines. Though this might take me a little while since the holiday is
>> ending soon :''-(
>
> Maybe the holiday is just beginning this year? Bumping this thread just
> in case ;)
Hi Ihor -- Sorry for the delayed response. I'm working on this now and
should have something ready for review in the coming days.
Best,
Jack
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2024-12-16 18:49 ` Ihor Radchenko
2024-12-28 6:09 ` Jack Kamm
@ 2024-12-29 9:18 ` Jack Kamm
2024-12-29 10:33 ` Ihor Radchenko
1 sibling, 1 reply; 11+ messages in thread
From: Jack Kamm @ 2024-12-29 9:18 UTC (permalink / raw)
To: Ihor Radchenko; +Cc: emacs-orgmode
[-- Attachment #1: Type: text/plain, Size: 1177 bytes --]
Ihor Radchenko <yantar92@posteo.net> writes:
> Jack Kamm <jackkamm@gmail.com> writes:
>
>>> The API of `org-datetree--find-create' is generally very limiting.
>>> It would be nice to come up with something less limiting.
>>
>> Thanks for the feedback -- I'll start working on something along these
>> lines. Though this might take me a little while since the holiday is
>> ending soon :''-(
>
> Maybe the holiday is just beginning this year? Bumping this thread just
> in case ;)
I attach a pair of patches for this.
The first patch is just a prelude, it adds a couple unit tests for bugs
I noticed in the current implementation.
The second patch is the main work. It is a substantial reworking of
org-datetree.el that allows for arbitrary number of datetree levels.
For capture datetrees, :tree-type can now be any subset of (year quarter
month week day), and a datetree with the corresponding levels will be
constructed. Another notable addition is the elisp function
`org-datetree-find-create-hierarchy', which should allow constructing
general datetrees for other calendar systems (e.g. lunar calendars,
university academic calendars, retail 4-4-5 calendars, etc).
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-org-datetree-Add-unit-tests-for-incorrect-sorting.patch --]
[-- Type: text/x-patch, Size: 2547 bytes --]
From b890687ec6732eaf90d4aa03c6ab450504a5988a Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sun, 29 Dec 2024 00:48:35 -0800
Subject: [PATCH 1/2] org-datetree: Add unit tests for incorrect sorting
*
testing/lisp/test-org-datetree.el (test-org-datetree/find-date-create):
Add test that a subtree is inserted in the correct location, even if
there exists another subtree that looks like a datetree.
(test-org-datetree/find-iso-week-create): Add test that days within a
week spanning 2 years are sorted correctly.
---
testing/lisp/test-org-datetree.el | 39 +++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/testing/lisp/test-org-datetree.el b/testing/lisp/test-org-datetree.el
index bd06462f2..620a916df 100644
--- a/testing/lisp/test-org-datetree.el
+++ b/testing/lisp/test-org-datetree.el
@@ -108,6 +108,30 @@ (ert-deftest test-org-datetree/find-date-create ()
(let ((org-datetree-add-timestamp nil))
(org-datetree-find-date-create '(3 29 2012)))
(org-trim (buffer-string)))))
+ ;; Insert at correct location, even if some other heading has a
+ ;; subtree that looks like a datetree
+ (should
+ (string-match
+ "\\`\\* Dummy heading
+
+\\*\\* 2012
+
+\\* 2012
+
+\\*\\* 2012-03 March
+
+\\*\\*\\* 2012-03-29 .*\\'"
+ (org-test-with-temp-text "\
+* Dummy heading
+
+** 2012
+
+* 2012
+
+** 2012-03 March"
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-date-create '(3 29 2012)))
+ (org-trim (buffer-string)))))
;; Always leave point at beginning of day entry.
(should
(string-match
@@ -188,6 +212,21 @@ (ert-deftest test-org-datetree/find-iso-week-create ()
(org-datetree-find-iso-week-create '(9 1 2015))
(org-datetree-find-iso-week-create '(12 31 2014)))
(org-trim (buffer-string)))))
+ ;; Sort new entry in correct order within its week when
+ ;; iso-week-year is not calendar year
+ (should
+ (string-match
+ "\\`\\* 2015
+
+\\*\\* 2015-W01
+
+\\*\\*\\* 2014-12-31 .*
+\\*\\*\\* 2015-01-01 .*"
+ (org-test-with-temp-text "* 2015"
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-iso-week-create '(1 1 2015))
+ (org-datetree-find-iso-week-create '(12 31 2014)))
+ (org-trim (buffer-string)))))
;; When `org-datetree-add-timestamp' is non-nil, insert a timestamp
;; in entry. When set to `inactive', insert an inactive one.
(should
--
2.47.1
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-org-datetree-Add-additional-tree-types-e.g.-quarter-.patch --]
[-- Type: text/x-patch, Size: 29206 bytes --]
From b8cc188103baec26c7af337417f8ef84c2af81da Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sun, 29 Dec 2024 00:52:59 -0800
Subject: [PATCH 2/2] org-datetree: Add additional tree types (e.g. quarter,
month+week)
* lisp/org-capture.el (org-capture-templates): Update docstring for
new datetree tree-type options.
(org-capture-set-target-location): Allow tree-type to be a set, and
switch to using `org-datetree-find-create-entry' to support this.
* lisp/org-datetree.el: Add requirements on cal-iso and org-element.
(org-datetree-find-date-create,org-datetree-find-month-create): Replace
`org-datetree--find-create-group' with `org-datetree-find-create-entry'.
(org-datetree--find-create-group): Removed in favor of
`org-datetree-find-create-entry'.
(org-datetree-find-iso-week-create): Turn into a wrapper for
`org-datetree-find-create-entry'.
(org-datetree-find-create-entry): Generalizes the now removed
`org-datetree--find-create-group' to handle more general tree type
sets. It is in turn a wrapper around
`org-datetree-find-create-hierarchy' which allows for constructing
other datetree hierarchies.
(org-datetree--compare-fun-from-regex): Generator for
string-comparison functions, used by `org-datetree-find-create-entry'
when calling `org-datetree-find-create-hierarchy'.
(org-datetree-find-create-hierarchy): New function that allows
constructing generic types of datetrees for other calendar systems.
(org-datetree-insert-line): Delete undocumented helper function.
(org-datetree--find-create-subheading): Generic replacement for
`org-datetree--find-create', that doesn't assume year/month/day
calendar system.
*
testing/lisp/test-org-datetree.el (test-org-datetree/find-quarter-month-create):
Test year-quarter-month datetree.
(test-org-datetree/find-quarter-month-day-create): Test
year-quarter-month-day datetree.
(test-org-datetree/find-quarter-week-create): Test year-quarter-week
datetree.
(test-org-datetree/find-month-week-create): Test year-month-week datetree.
---
doc/org-manual.org | 13 +-
etc/ORG-NEWS | 43 ++++
lisp/org-capture.el | 13 +-
lisp/org-datetree.el | 372 +++++++++++++++++-------------
testing/lisp/test-org-datetree.el | 48 ++++
5 files changed, 325 insertions(+), 164 deletions(-)
diff --git a/doc/org-manual.org b/doc/org-manual.org
index 1b3c33f96..93786f3f3 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -8177,10 +8177,15 @@ Now lets look at the elements of a template definition. Each entry in
- ~:tree-type~ ::
- Use ~week~ to make a week tree instead of the month-day tree,
- i.e., place the headings for each day under a heading with the
- current ISO week. Use ~month~ to group entries by month
- only. Default is to group entries by day.
+ Default is to group entries by day. Use ~week~ to make a week
+ tree instead of the month-day tree, i.e., place the headings for
+ each day under a heading with the current ISO week. Use ~month~
+ to group entries by month only. Use any subset of ~(year quarter
+ month week day)~ to group by the specified levels. In case
+ ~month~ and ~week~ are both specified, weeks are assigned to the
+ month containing Thursday, to be consistent with the ISO year-week
+ rule. In case ~quarter~ and ~week~ but not ~month~ are specified,
+ quarters are 13-week periods; otherwise they are 3-month periods.
- ~:unnarrowed~ ::
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 85411ecc1..eb9967e96 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -141,6 +141,30 @@ See the new [[info:org#Repeating commands]["Repeating commands"]] section in Org
Tables copied into the clipboard from LibreOffice Calc documents can
now be pasted as an Org table using ~yank-media~.
+*** New datetree capture ~:tree-type~ options
+:PROPERTIES:
+:CUSTOM_ID: 9.8-datetree-treetype
+:END:
+
+For datetree capture, ~:tree-type~ can now be any subset of ~(year
+quarter month week day)~ to construct a datetree with the specified
+levels. For back-compatibility, the default value of ~nil~ is an
+alias for ~(year month day)~, ~month~ is an alias for ~(year month)~,
+and ~week~ is an alias for ~(year week day)~.
+
+If ~:tree-type~ is a superset of ~(month week)~, then weeks are
+assigned to the month containing Thursday, to be consistent with the
+ISO-8601 year-week rule. If ~:tree-type~ contains ~(quarter week)~
+but does not contain ~month~, then quarters are defined as 13-week
+periods (the final quarter of a 53-week year has 14-weeks).
+Otherwise, quarters are defined as 3-month periods.
+
+Furthermore, the new elisp function ~org-datetree-find-create-entry~
+generalizes ~org-datetree-find-date-create~,
+~org-datetree-find-month-create~, and
+~org-datetree-find-iso-week-create~ to handle the new available
+datetree hierarchies.
+
** New and changed options
# Changes deadling with changing default values of customizations,
@@ -281,6 +305,18 @@ leave extra prompts after evaluation, and skipping the prompt
filtering can be more robust for such languages (as this avoids
removing false positive prompts).
+*** Elisp functions for new datetree tree-types
+
+Accompanying the [[#9.8-datetree-treetype][new datetree capture ~:tree-type~ options]], on the
+elisp level ~org-datetree-find-create-entry~ provides the new tree
+type options to generalize ~org-datetree-find-date-create~,
+~org-datetree-find-month-create~, and
+~org-datetree-find-iso-week-create~.
+
+In addition, ~org-datetree-find-create-hierarchy~ provides a mechanism
+for constructing datetrees for other calendar systems (e.g. lunar
+calendar, school semesters, the retail 4-4-5 calendar, etc).
+
** Removed or renamed functions and variables
*** ~org-cycle-display-inline-images~ is renamed to ~org-cycle-display-link-previews~
@@ -299,6 +335,13 @@ previews of supported link types besides image links.
The behavior is unchanged, except in that the new variable now affects
previews of supported link types besides image links.
+*** Obsolete functions and variables removed from ~org-datetree~
+
+Due to the refactoring of ~org-datetree~ to support the [[#9.8-datetree-treetype][new datetree
+capture ~:tree-type~ options]], the internal variable
+~org-datetree-base-level~ has been removed, as well as the
+undocumented helper function ~org-datetree-insert-line~.
+
** Miscellaneous
*** Org mode no longer prevents =flyspell= from spell-checking inside =LOGBOOK= drawers
diff --git a/lisp/org-capture.el b/lisp/org-capture.el
index 486304df2..5d6f1df2d 100644
--- a/lisp/org-capture.el
+++ b/lisp/org-capture.el
@@ -293,7 +293,9 @@ (defcustom org-capture-templates nil
:tree-type When `week', make a week tree instead of the month-day
tree. When `month', make a month tree instead of the
- month-day tree.
+ month-day tree. When any subset of
+ `(year quarter month week day)', create a datetree
+ hierarchy with the specified levels.
:unnarrowed Do not narrow the target buffer, simply show the
full buffer. Default is to narrow it so that you
@@ -1090,10 +1092,13 @@ (defun org-capture-set-target-location (&optional target)
;; yesterday, if we are extending dates for a couple of
;; hours)
(funcall
+ #'org-datetree-find-create-entry
(pcase (org-capture-get :tree-type)
- (`week #'org-datetree-find-iso-week-create)
- (`month #'org-datetree-find-month-create)
- (_ #'org-datetree-find-date-create))
+ (`week '(year week day))
+ (`month '(year month))
+ (`day '(year month day))
+ ((pred not) '(year month day))
+ (grouping grouping))
(calendar-gregorian-from-absolute
(cond
(org-overriding-default-time
diff --git a/lisp/org-datetree.el b/lisp/org-datetree.el
index d0cc1fabb..7101cbf93 100644
--- a/lisp/org-datetree.el
+++ b/lisp/org-datetree.el
@@ -24,23 +24,20 @@
;;
;;; Commentary:
-;; This file contains code to create entries in a tree where the top-level
-;; nodes represent years, the level 2 nodes represent the months, and the
-;; level 1 entries days.
+;; This file contains code to create entries in a tree where the
+;; top-level nodes represent years, the level 2 nodes represent the
+;; months, and the level 1 entries days. It also implements
+;; extensions to the datetree that allow for other levels such as
+;; quarters and weeks.
;;; Code:
(require 'org-macs)
(org-assert-version)
+(require 'cal-iso)
(require 'org)
-
-(defvar org-datetree-base-level 1
- "The level at which years should be placed in the date tree.
-This is normally one, but if the buffer has an entry with a
-DATE_TREE (or WEEK_TREE for ISO week entries) property (any
-value), the date tree will become a subtree under that entry, so
-the base level will be properly adjusted.")
+(require 'org-element)
(defcustom org-datetree-add-timestamp nil
"When non-nil, add a time stamp matching date of entry.
@@ -59,174 +56,237 @@ (defun org-datetree-find-date-create (d &optional keep-restriction)
When it is nil, the buffer will be widened to make sure an existing date
tree can be found. If it is the symbol `subtree-at-point', then the tree
will be built under the headline at point."
- (org-datetree--find-create-group d 'day keep-restriction))
+ (org-datetree-find-create-entry '(year month day) d keep-restriction))
;;;###autoload
(defun org-datetree-find-month-create (d &optional keep-restriction)
"Find or create a month entry for date D.
Compared to `org-datetree-find-date-create' this function creates
-entries grouped by month instead of days.
+entries grouped by year-month instead of year-month-day.
If KEEP-RESTRICTION is non-nil, do not widen the buffer.
When it is nil, the buffer will be widened to make sure an existing date
tree can be found. If it is the symbol `subtree-at-point', then the tree
will be built under the headline at point."
- (org-datetree--find-create-group d 'month keep-restriction))
-
-(defun org-datetree--find-create-group
- (d time-grouping &optional keep-restriction)
- "Find or create an entry for date D.
-If time-period is day, group entries by day.
-If time-period is month, then group entries by month."
- (setq-local org-datetree-base-level 1)
- (save-restriction
- (if (eq keep-restriction 'subtree-at-point)
- (progn
- (unless (org-at-heading-p) (error "Not at heading"))
- (widen)
- (org-narrow-to-subtree)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1)))
- (unless keep-restriction (widen))
- ;; Support the old way of tree placement, using a property
- (let ((prop (org-find-property "DATE_TREE")))
- (when prop
- (goto-char prop)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1))
- (org-narrow-to-subtree))))
- (goto-char (point-min))
- (let ((year (calendar-extract-year d))
- (month (calendar-extract-month d))
- (day (calendar-extract-day d)))
- (org-datetree--find-create
- "\\([12][0-9]\\{3\\}\\)"
- year nil nil nil t)
- (org-datetree--find-create
- "%d-\\([01][0-9]\\) \\w+"
- year month nil nil t)
- (when (eq time-grouping 'day)
- (org-datetree--find-create
- "%d-%02d-\\([0123][0-9]\\) \\w+"
- year month day nil t)))))
+ (org-datetree-find-create-entry '(year month) d keep-restriction))
;;;###autoload
(defun org-datetree-find-iso-week-create (d &optional keep-restriction)
"Find or create an ISO week entry for date D.
Compared to `org-datetree-find-date-create' this function creates
-entries ordered by week instead of months.
-When it is nil, the buffer will be widened to make sure an existing date
-tree can be found. If it is the symbol `subtree-at-point', then the tree
-will be built under the headline at point."
- (setq-local org-datetree-base-level 1)
- (save-restriction
- (if (eq keep-restriction 'subtree-at-point)
- (progn
- (unless (org-at-heading-p) (error "Not at heading"))
- (widen)
- (org-narrow-to-subtree)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1)))
- (unless keep-restriction (widen))
- ;; Support the old way of tree placement, using a property
- (let ((prop (org-find-property "WEEK_TREE")))
- (when prop
- (goto-char prop)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1))
- (org-narrow-to-subtree))))
- (goto-char (point-min))
- (require 'cal-iso)
- (let* ((year (calendar-extract-year d))
- (month (calendar-extract-month d))
- (day (calendar-extract-day d))
- (time (org-encode-time 0 0 0 day month year))
- (iso-date (calendar-iso-from-absolute
- (calendar-absolute-from-gregorian d)))
- (weekyear (nth 2 iso-date))
- (week (nth 0 iso-date)))
- ;; ISO 8601 week format is %G-W%V(-%u)
- (org-datetree--find-create
- "\\([12][0-9]\\{3\\}\\)"
- weekyear nil nil (format-time-string "%G" time) t)
- (org-datetree--find-create
- "%d-W\\([0-5][0-9]\\)"
- weekyear week nil (format-time-string "%G-W%V" time) t)
- ;; For the actual day we use the regular date instead of ISO week.
- (org-datetree--find-create
- "%d-%02d-\\([0123][0-9]\\) \\w+" year month day nil t))))
+entries grouped by year-week-day instead of year-month-day. If
+KEEP-RESTRICTION is non-nil, do not widen the buffer. When it is
+nil, the buffer will be widened to make sure an existing date
+tree can be found. If it is the symbol `subtree-at-point', then
+the tree will be built under the headline at point."
+ (org-datetree-find-create-entry '(year week day) d keep-restriction))
-(defun org-datetree--find-create
- (regex-template year &optional month day insert match-title)
- "Find the datetree matched by REGEX-TEMPLATE for YEAR, MONTH, or DAY.
-REGEX-TEMPLATE is passed to `format' with YEAR, MONTH, and DAY as
-arguments.
+;;;###autoload
+(defun org-datetree-find-create-entry
+ (time-grouping d &optional keep-restriction)
+ "Find or create an entry for date D.
+TIME-GROUPING specifies the grouping levels of the datetree, and
+should be a subset of `(year quarter month week day)'. Weeks are
+assigned to years according to ISO-8601. If TIME-GROUPING
+contains both `month' and `week', then weeks are assigned to the
+month containing Thursday, for consistency with the ISO-8601
+year-week rule. If TIME-GROUPING contains `quarter' and `week'
+but not `month', quarters are defined as 13-week periods;
+otherwise they are defined as 3-month periods.
-If MATCH-TITLE is non-nil, REGEX-TEMPLATE is matched against
-heading title and the exact regexp matched against heading line is:
+If KEEP-RESTRICTION is non-nil, do not widen the buffer. When it
+is nil, the buffer will be widened to make sure an existing date
+tree can be found. If it is the symbol `subtree-at-point', then
+the tree will be built under the headline at point."
+ (let* ((year (calendar-extract-year d))
+ (month (calendar-extract-month d))
+ (day (calendar-extract-day d))
+ (time (org-encode-time 0 0 0 day month year))
+ (iso-date (calendar-iso-from-absolute
+ (calendar-absolute-from-gregorian d)))
+ (week (nth 0 iso-date))
+ (nominal-year
+ (if (memq 'week time-grouping)
+ (nth 2 iso-date)
+ year))
+ (nominal-month
+ (if (memq 'week time-grouping)
+ (calendar-extract-month
+ ;; anchor on Thurs, to be consistent with weekyear
+ (calendar-gregorian-from-absolute
+ (calendar-iso-to-absolute
+ `(,week 4 ,nominal-year))))
+ month))
+ (quarter (if (and (memq 'week time-grouping)
+ (not (memq 'month time-grouping)))
+ (min 4 (1+ (/ (1- week) 13)))
+ (1+ (/ (1- nominal-month) 3)))))
+ (org-datetree-find-create-hierarchy
+ (append
+ (when (memq 'year time-grouping)
+ (list (list (number-to-string nominal-year)
+ (org-datetree--compare-fun-from-regex
+ "\\([12][0-9]\\{3\\}\\)"))))
+ (when (memq 'quarter time-grouping)
+ (list (list (format "%d-Q%d" nominal-year quarter)
+ (org-datetree--compare-fun-from-regex
+ "\\([12][0-9]\\{3\\}-Q[1-4]\\)"))))
+ (when (memq 'month time-grouping)
+ (list (list (format-time-string
+ "%Y-%m %B" (org-encode-time 0 0 0 1 nominal-month
+ nominal-year))
+ (org-datetree--compare-fun-from-regex
+ "\\([12][0-9]\\{3\\}-[01][0-9]\\) \\w+"))))
+ (when (memq 'week time-grouping)
+ (list (list (format-time-string "%G-W%V" time)
+ (org-datetree--compare-fun-from-regex
+ "\\([12][0-9]\\{3\\}-W[0-5][0-9]\\)"))))
+ (when (memq 'day time-grouping)
+ ;; Use regular date instead of ISO-week year/month
+ (list (list (format-time-string
+ "%Y-%m-%d %A" (org-encode-time 0 0 0 day month year))
+ (org-datetree--compare-fun-from-regex
+ "\\([12][0-9]\\{3\\}-[01][0-9]-[0123][0-9]\\) \\w+")))))
+ keep-restriction
+ ;; Support the old way of tree placement, using a property
+ (cond
+ ((seq-set-equal-p time-grouping '(year month day))
+ "DATE_TREE")
+ ((seq-set-equal-p time-grouping '(year month))
+ "DATE_TREE")
+ ((seq-set-equal-p time-grouping '(year week day))
+ "WEEK_TREE")))
+ (when (memq 'day time-grouping)
+ (when org-datetree-add-timestamp
+ (save-excursion
+ (end-of-line)
+ (insert "\n")
+ (org-indent-line)
+ (org-insert-timestamp
+ (org-encode-time 0 0 0 day month year)
+ nil
+ (eq org-datetree-add-timestamp 'inactive)))))))
- (format org-complex-heading-regexp-format
- (format regex-template year month day))
+(defun org-datetree--compare-fun-from-regex (sibling-regex)
+ "Construct comparison function based on regular expression.
+SIBLING-REGEX should be a regex that matches the headline and its
+siblings, with 1 match group. Headlines are compared by the
+lexicographic ordering of match group 1."
+ (lambda (sibling-title new-title)
+ (let ((target-match (and (string-match sibling-regex new-title)
+ (match-string 1 new-title)))
+ (sibling-match (and (string-match sibling-regex sibling-title)
+ (match-string 1 sibling-title))))
+ (cond
+ ((not (and target-match sibling-match)) nil)
+ ((string< sibling-match target-match) -1)
+ ((string> sibling-match target-match) 1)
+ (t 0)))))
-If MATCH-TITLE is nil, the regexp matched against heading line is
-REGEX-TEMPLATE:
+(defun org-datetree-find-create-hierarchy
+ (hier-pairs &optional keep-restriction legacy-prop)
+ "Insert a new entry into a datetree from the entry's full date hierarchy.
+HIER-PAIRS is a list whose first entry corresponds to the outermost element
+(e.g. year) and last entry corresponds to the innermost (e.g. day).
+Each entry of the list is a pair, the car is the headline for that level
+(e.g. \"2024\" or \"2024-12-28\"), and the cadr is a string
+comparison function for sorting each headline among its siblings.
+The comparison function should take 2 arguments, corresponding to
+the titles of 2 headlines, and return a negative number of the
+first headline precedes the second, a positive number of the
+second has precedence, 0 if the headlines are at the same time,
+or `nil' if a headline isn't a valid datetree subheading. For
+example, HIER-PAIRS could look like
- (format regex-template year month day)
+ ((\"2024\" compare-year-fun)
+ (\"2024-12 December\" compare-month-fun)
+ (\"2024-12-28 Saturday\" compare-day-fun))
-Match group 1 in REGEX-TEMPLATE is compared against the specified date
-component. If INSERT is non-nil and there is no match then it is
-inserted into the buffer."
- (when (or month day)
- (org-narrow-to-subtree))
- ;; ensure that the first match group in REGEX-TEMPLATE
- ;; is the first inside `org-complex-heading-regexp-format'
- (when (and match-title
- (not (string-match-p "\\\\(\\?1:" regex-template))
- (string-match "\\\\(" regex-template))
- (setq regex-template (replace-match "\\(?1:" nil t regex-template)))
- (let ((re (if match-title
- (format org-complex-heading-regexp-format
- (format regex-template year month day))
- (format regex-template year month day)))
- match)
- (goto-char (point-min))
- (while (and (setq match (re-search-forward re nil t))
- (goto-char (match-beginning 1))
- (< (string-to-number (match-string 1)) (or day month year))))
- (cond
- ((not match)
- (goto-char (point-max))
- (unless (bolp) (insert "\n"))
- (org-datetree-insert-line year month day insert))
- ((= (string-to-number (match-string 1)) (or day month year))
- (forward-line 0))
- (t
- (forward-line 0)
- (org-datetree-insert-line year month day insert)))))
+where compare-month-fun would be some function where
+(compare-month-fun \"2024-12-December\" \"2024-12-November\") is
+negative, and (compare-month-fun \"2024-12-December\" \"Potato\")
+is nil.
-(defun org-datetree-insert-line (year &optional month day text)
- (delete-region (save-excursion (skip-chars-backward " \t\n") (point)) (point))
- (when (org--blank-before-heading-p) (insert "\n"))
- (insert "\n" (make-string org-datetree-base-level ?*) " \n")
- (backward-char)
- (when month (org-do-demote))
- (when day (org-do-demote))
- (if text
- (insert text)
- (insert (format "%d" year))
- (when month
+If KEEP-RESTRICTION is non-nil, do not widen the buffer.
+When it is nil, the buffer will be widened to make sure an existing date
+tree can be found. If it is the symbol `subtree-at-point', then the tree
+will be built under the headline at point.
+
+If LEGACY-PROP is non-nil, the tree is located by searching for a
+headline with property LEGACY-PROP, supporting the old way of
+tree placement via a property."
+ (let (tree)
+ (save-restriction
+ ;; get the datetree base and narrow to it
+ (if (eq keep-restriction 'subtree-at-point)
+ (progn
+ (unless (org-at-heading-p) (error "Not at heading"))
+ (widen)
+ (org-narrow-to-subtree)
+ (setq tree (car (org-element-contents (org-element-parse-buffer 'headline)))))
+ (unless keep-restriction (widen))
+ ;; Support the old way of tree placement, using a property
+ (let ((prop (and legacy-prop (org-find-property legacy-prop))))
+ (if prop
+ (progn
+ (goto-char prop)
+ (org-narrow-to-subtree)
+ (setq tree (car (org-element-contents (org-element-parse-buffer 'headline)))))
+ (setq tree (org-element-parse-buffer)))))
+ (cl-loop
+ for pair in hier-pairs
+ do
+ (setq tree
+ (org-datetree--find-create-subheading
+ (cadr pair) (car pair) tree)))
+ tree)))
+
+(defun org-datetree--find-create-subheading
+ (compare-fun new-title tree)
+ "Find datetree subheading, or create it if it doesn't exist.
+After insertion, move point to beginning of the subheading, and
+narrow to its subtree. NEW-TITLE is the subheading to be found
+or created. TREE is the parent headline, or an element of type
+`org-data' if NEW-TITLE is to be at level 1. COMPARE-FUN is a
+function of 2 arguments for comparing headline titles; it should
+return a negative number if the first headline precedes the
+second, a positive number if the second number has precedence, 0
+if the headlines are at the same time, and `nil' if a headline
+isn't a valid datetree subheading at this level."
+ (let* ((level (if (eq (org-element-type tree) 'org-data)
+ 1
+ (1+ (org-element-property :level tree))))
+ (sibling (org-element-map tree 'headline
+ (lambda (d)
+ (when (= (org-element-property :level d) level)
+ (let ((compare-result
+ (funcall compare-fun
+ (org-element-property :raw-value d)
+ new-title)))
+ (and compare-result (>= compare-result 0) d))))
+ nil t)))
+ ;; go to headline, or first successor sibling, or end of buffer
+ (if sibling
+ (goto-char (org-element-property :begin sibling))
+ (goto-char (point-max))
+ (unless (bolp) (insert "\n")))
+ (if (and sibling
+ (= 0 (funcall compare-fun
+ (org-element-property :raw-value sibling)
+ new-title)))
+ ;; narrow and return the matched headline
+ (progn
+ (org-narrow-to-subtree)
+ sibling)
+ ;; insert new headline, narrow, and return it
+ (delete-region (save-excursion (skip-chars-backward " \t\n") (point)) (point))
+ (when (org--blank-before-heading-p) (insert "\n"))
(insert
- (if day
- (format-time-string "-%m-%d %A" (org-encode-time 0 0 0 day month year))
- (format-time-string "-%m %B" (org-encode-time 0 0 0 1 month year))))))
- (when (and day org-datetree-add-timestamp)
- (save-excursion
- (insert "\n")
- (org-indent-line)
- (org-insert-timestamp
- (org-encode-time 0 0 0 day month year)
- nil
- (eq org-datetree-add-timestamp 'inactive))))
- (forward-line 0))
+ (format "\n%s %s\n"
+ (make-string (if org-odd-levels-only (1- (* 2 level)) level) ?*)
+ new-title))
+ (forward-line -1)
+ (org-narrow-to-subtree)
+ (org-element-at-point))))
(defun org-datetree-file-entry-under (txt d)
"Insert a node TXT into the date tree under date D."
diff --git a/testing/lisp/test-org-datetree.el b/testing/lisp/test-org-datetree.el
index 620a916df..585bd692c 100644
--- a/testing/lisp/test-org-datetree.el
+++ b/testing/lisp/test-org-datetree.el
@@ -160,6 +160,54 @@ (ert-deftest test-org-datetree/find-month-create ()
(org-datetree-find-month-create '(3 29 2012)))
(org-trim (buffer-string)))))))
+(ert-deftest test-org-datetree/find-quarter-month-create ()
+ "Test `org-datetree-find-quarter-month-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2012\n\n\\*\\* 2012-Q1\n\n\\*\\*\\* 2012-03 .*\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-create-entry '(year quarter month) '(3 29 2012)))
+ (org-trim (buffer-string)))))))
+
+(ert-deftest test-org-datetree/find-quarter-month-day-create ()
+ "Test `org-datetree-find-quarter-month-day-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2012\n\n\\*\\* 2012-Q1\n\n\\*\\*\\* 2012-03 .*\n\n\\*\\*\\*\\* 2012-03-29 .*\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-create-entry '(year quarter month day) '(3 29 2012)))
+ (org-trim (buffer-string)))))))
+
+(ert-deftest test-org-datetree/find-quarter-week-create ()
+ "Test `org-datetree-find-quarter-week-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2024\n\n\\*\\* 2024-Q4\n\n\\*\\*\\* 2024-W52\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-create-entry '(year quarter week) '(12 27 2024)))
+ (org-trim (buffer-string)))))))
+
+(ert-deftest test-org-datetree/find-month-week-create ()
+ "Test `org-datetree-find-month-week-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2024\n\n\\*\\* 2024-12 .*\n\n\\*\\*\\* 2024-W52\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-create-entry '(year month week) '(12 27 2024)))
+ (org-trim (buffer-string)))))))
+
(ert-deftest test-org-datetree/find-iso-week-create ()
"Test `org-datetree-find-iso-date-create' specification."
(let ((org-blank-before-new-entry '((heading . t))))
--
2.47.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2024-12-29 9:18 ` Jack Kamm
@ 2024-12-29 10:33 ` Ihor Radchenko
2024-12-30 16:20 ` Jack Kamm
2024-12-31 1:56 ` Jack Kamm
0 siblings, 2 replies; 11+ messages in thread
From: Ihor Radchenko @ 2024-12-29 10:33 UTC (permalink / raw)
To: Jack Kamm; +Cc: emacs-orgmode
Jack Kamm <jackkamm@gmail.com> writes:
> I attach a pair of patches for this.
Thanks! See my comments inline.
> +(defun org-datetree-find-create-entry
> + (time-grouping d &optional keep-restriction)
> + "Find or create an entry for date D.
> +TIME-GROUPING specifies the grouping levels of the datetree, and
> +should be a subset of `(year quarter month week day)'. Weeks are
> +assigned to years according to ISO-8601. If TIME-GROUPING
> +contains both `month' and `week', then weeks are assigned to the
> +month containing Thursday, for consistency with the ISO-8601
> +year-week rule. If TIME-GROUPING contains `quarter' and `week'
> +but not `month', quarters are defined as 13-week periods;
> +otherwise they are defined as 3-month periods.
>
> -If MATCH-TITLE is non-nil, REGEX-TEMPLATE is matched against
> -heading title and the exact regexp matched against heading line is:
> +If KEEP-RESTRICTION is non-nil, do not widen the buffer. When it
> +is nil, the buffer will be widened to make sure an existing date
> +tree can be found. If it is the symbol `subtree-at-point', then
> +the tree will be built under the headline at point."
Please also document how `org-datetree-add-timestamp' affects this function.
> + ;; Support the old way of tree placement, using a property
> + (cond
> + ((seq-set-equal-p time-grouping '(year month day))
> + "DATE_TREE")
> + ((seq-set-equal-p time-grouping '(year month))
> + "DATE_TREE")
> + ((seq-set-equal-p time-grouping '(year week day))
> + "WEEK_TREE")))
It would be a good idea to add a few tests for this scenario.
To make sure that refactoring did not break things.
> + ;; Support the old way of tree placement, using a property
> + (let ((prop (and legacy-prop (org-find-property legacy-prop))))
> + (if prop
> + (progn
> + (goto-char prop)
> + (org-narrow-to-subtree)
> + (setq tree (car (org-element-contents (org-element-parse-buffer 'headline)))))
> + (setq tree (org-element-parse-buffer)))))
Why do you need object granularity by default (second call to
`org-element-parse-buffer')?
Also, more importantly, do you have to run the full parsing here? Maybe
utilize `org-element-cache-map' instead? Full parsing is going to be
much slower.
> + (cl-loop
> + for pair in hier-pairs
> + do
> + (setq tree
> + (org-datetree--find-create-subheading
> + (cadr pair) (car pair) tree)))
It is undocumented in the `org-datetree--find-create-subheading'
docstring that it returns something.
--
Ihor Radchenko // yantar92,
Org mode maintainer,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2024-12-29 10:33 ` Ihor Radchenko
@ 2024-12-30 16:20 ` Jack Kamm
2024-12-30 17:11 ` Ihor Radchenko
2024-12-31 1:56 ` Jack Kamm
1 sibling, 1 reply; 11+ messages in thread
From: Jack Kamm @ 2024-12-30 16:20 UTC (permalink / raw)
To: Ihor Radchenko; +Cc: emacs-orgmode
Ihor Radchenko <yantar92@posteo.net> writes:
>> + ;; Support the old way of tree placement, using a property
>> + (let ((prop (and legacy-prop (org-find-property legacy-prop))))
>> + (if prop
>> + (progn
>> + (goto-char prop)
>> + (org-narrow-to-subtree)
>> + (setq tree (car (org-element-contents (org-element-parse-buffer 'headline)))))
>> + (setq tree (org-element-parse-buffer)))))
>
> Why do you need object granularity by default (second call to
> `org-element-parse-buffer')?
> Also, more importantly, do you have to run the full parsing here? Maybe
> utilize `org-element-cache-map' instead? Full parsing is going to be
> much slower.
We don't need object granularity, that was an oversight on my part --
should have specified headline granularity.
Does `org-element-cache-map' traverse elements in the order they're in
the buffer? That is something we need for this.
On my working branch I have an earlier commit that implements many of
the improvements here but using the old regexp search way instead of the
org-element way. Would it be worth reverting to that point?
Specifically, the new `org-datetree-find-create-entry' that allows for
nested years/quarters/months/weeks/days is still pretty straightforward
to implement in the regexp approach. The more general
`org-datetree-find-create-hierarchy' (that allows elisp hackers to build
new kinds of datetrees) might be trickier without org-element, but we
could also defer that for future work.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2024-12-30 16:20 ` Jack Kamm
@ 2024-12-30 17:11 ` Ihor Radchenko
0 siblings, 0 replies; 11+ messages in thread
From: Ihor Radchenko @ 2024-12-30 17:11 UTC (permalink / raw)
To: Jack Kamm; +Cc: emacs-orgmode
Jack Kamm <jackkamm@gmail.com> writes:
> Does `org-element-cache-map' traverse elements in the order they're in
> the buffer? That is something we need for this.
Yes, it does.
You can even edit buffer along the way.
`org-element-cache-map' combines regexp search and parser cache trying
to be as fast as possible.
> On my working branch I have an earlier commit that implements many of
> the improvements here but using the old regexp search way instead of the
> org-element way. Would it be worth reverting to that point?
> Specifically, the new `org-datetree-find-create-entry' that allows for
> nested years/quarters/months/weeks/days is still pretty straightforward
> to implement in the regexp approach. The more general
> `org-datetree-find-create-hierarchy' (that allows elisp hackers to build
> new kinds of datetrees) might be trickier without org-element, but we
> could also defer that for future work.
I believe that `org-element-cache-map' should be good enough to do
everything.
--
Ihor Radchenko // yantar92,
Org mode maintainer,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2024-12-29 10:33 ` Ihor Radchenko
2024-12-30 16:20 ` Jack Kamm
@ 2024-12-31 1:56 ` Jack Kamm
2025-01-01 9:14 ` Ihor Radchenko
1 sibling, 1 reply; 11+ messages in thread
From: Jack Kamm @ 2024-12-31 1:56 UTC (permalink / raw)
To: Ihor Radchenko; +Cc: emacs-orgmode
[-- Attachment #1: Type: text/plain, Size: 2075 bytes --]
Thanks for the feedback. I attach a squashed updated patch for part 2.
You can also see the unsquashed changes at https://github.com/jackkamm/org-mode/tree/2024-grouped-weektree-rebase
>> +(defun org-datetree-find-create-entry
> Please also document how `org-datetree-add-timestamp' affects this function.
Done. On reviewing this I also found a bug (datestamp added again if
entry already existed) -- I fixed it and added a unit test.
>> + ;; Support the old way of tree placement, using a property
>> + (cond
>> + ((seq-set-equal-p time-grouping '(year month day))
>> + "DATE_TREE")
>> + ((seq-set-equal-p time-grouping '(year month))
>> + "DATE_TREE")
>> + ((seq-set-equal-p time-grouping '(year week day))
>> + "WEEK_TREE")))
>
> It would be a good idea to add a few tests for this scenario.
> To make sure that refactoring did not break things.
There was already a couple tests for DATE_TREE and WEEK_TREE, but I've
added a few more now, mainly around finding existing headings (the
previous tests only created new headings under the DATE_TREE or
WEEK_TREE).
> Why do you need object granularity by default (second call to
> `org-element-parse-buffer')?
> Also, more importantly, do you have to run the full parsing here? Maybe
> utilize `org-element-cache-map' instead? Full parsing is going to be
> much slower.
I've switched to `org-element-cache-map' now -- thanks for the info
about it.
> It is undocumented in the `org-datetree--find-create-subheading'
> docstring that it returns something.
I changed the return behavior and documented it. It now returns non-nil
if the subheading already exists -- this is needed to prevent
adding a datestamp twice when `org-datetree-add-timestamp'.
Finally, I made one more substantial change -- I now allow :tree-type to
be a function in the org-capture template. This allows using new types
of datetrees from `org-datetree-find-create-hierarchy' with org-capture.
Relatedly, I made `org-datetree-comparefun-from-regex' public to help
with building new types of datetrees.
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0002-org-datetree-Add-additional-tree-types-e.g.-quarter-.patch --]
[-- Type: text/x-patch, Size: 39212 bytes --]
From b8447f23c5618239bb3926a59d111fd21d985afe Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sun, 29 Dec 2024 00:52:59 -0800
Subject: [PATCH 2/2] org-datetree: Add additional tree types (e.g. quarter,
month+week)
* lisp/org-capture.el: Declare `org-datetree-find-create-hierarchy'.
(org-capture-templates): Update docstring for
new datetree tree-type options.
(org-capture-set-target-location): Allow tree-type to be a set
or function, and call `org-datetree-find-create-entry' or
`org-datetree-find-create-hierarchy' in those cases.
* lisp/org-datetree.el: Add requirements on cal-iso and org-element.
(org-datetree-find-date-create,org-datetree-find-month-create): Replace
`org-datetree--find-create-group' with `org-datetree-find-create-entry'.
(org-datetree--find-create-group): Removed in favor of
`org-datetree-find-create-entry'.
(org-datetree-find-iso-week-create): Turn into a wrapper for
`org-datetree-find-create-entry'.
(org-datetree-find-create-entry): Generalizes the now removed
`org-datetree--find-create-group' to handle more general tree type
sets. It is in turn a wrapper around
`org-datetree-find-create-hierarchy' which allows for constructing
other datetree hierarchies.
(org-datetree-comparefun-from-regex): Generator for
string-comparison functions, used by `org-datetree-find-create-entry'
when calling `org-datetree-find-create-hierarchy'.
(org-datetree-find-create-hierarchy): New function that allows
constructing generic types of datetrees for other calendar systems.
(org-datetree-insert-line): Delete undocumented helper function.
(org-datetree--find-create-subheading): Generic replacement for
`org-datetree--find-create', that doesn't assume year/month/day
calendar system.
* testing/lisp/test-org-capture.el (test-org-capture/entry): Add tests
for datetree capture with list or function :tree-type.
*
testing/lisp/test-org-datetree.el (test-org-datetree/find-date-create):
Add test to not add the timestamp twice. Add additional test for
legacy DATE_TREE method.
(test-org-datetree/find-month-create): Add tests for legacy DATE_TREE
method.
(test-org-datetree/find-quarter-month-create):
Test year-quarter-month datetree.
(test-org-datetree/find-quarter-month-day-create): Test
year-quarter-month-day datetree.
(test-org-datetree/find-quarter-week-create): Test year-quarter-week
datetree.
(test-org-datetree/find-month-week-create): Test year-month-week datetree.
(test-org-datetree/find-iso-week-create): Additional test for legacy
WEEK_TREE method.
---
doc/org-manual.org | 18 +-
etc/ORG-NEWS | 39 +++
lisp/org-capture.el | 27 +-
lisp/org-datetree.el | 399 ++++++++++++++++++------------
testing/lisp/test-org-capture.el | 45 ++++
testing/lisp/test-org-datetree.el | 103 +++++++-
6 files changed, 466 insertions(+), 165 deletions(-)
diff --git a/doc/org-manual.org b/doc/org-manual.org
index 1b3c33f96..f59f46f91 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -8177,10 +8177,20 @@ Now lets look at the elements of a template definition. Each entry in
- ~:tree-type~ ::
- Use ~week~ to make a week tree instead of the month-day tree,
- i.e., place the headings for each day under a heading with the
- current ISO week. Use ~month~ to group entries by month
- only. Default is to group entries by day.
+ Default is to group entries by day. Use ~week~ to make a week
+ tree instead of the month-day tree, i.e., place the headings for
+ each day under a heading with the current ISO week. Use ~month~
+ to group entries by month only. Use any subset of ~(year quarter
+ month week day)~ to group by the specified levels. In case
+ ~month~ and ~week~ are both specified, weeks are assigned to the
+ month containing Thursday, to be consistent with the ISO year-week
+ rule. In case ~quarter~ and ~week~ but not ~month~ are specified,
+ quarters are 13-week periods; otherwise they are 3-month periods.
+
+ #+findex: org-datetree-find-create-hierarchy
+ ~:tree-type~ can also be a function, in which it should take the
+ date as an argument and generate a list of pairs for
+ ~org-datetree-find-create-hierarchy~.
- ~:unnarrowed~ ::
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 85411ecc1..8779bdad1 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -141,6 +141,30 @@ See the new [[info:org#Repeating commands]["Repeating commands"]] section in Org
Tables copied into the clipboard from LibreOffice Calc documents can
now be pasted as an Org table using ~yank-media~.
+*** New datetree capture ~:tree-type~ options
+:PROPERTIES:
+:CUSTOM_ID: 9.8-datetree-treetype
+:END:
+
+For datetree capture, ~:tree-type~ can now be any subset of ~(year
+quarter month week day)~ to construct a datetree with the specified
+levels. For back-compatibility, the default value of ~nil~ is an
+alias for ~(year month day)~, ~month~ is an alias for ~(year month)~,
+and ~week~ is an alias for ~(year week day)~.
+
+If ~:tree-type~ is a superset of ~(month week)~, then weeks are
+assigned to the month containing Thursday, to be consistent with the
+ISO-8601 year-week rule. If ~:tree-type~ contains ~(quarter week)~
+but does not contain ~month~, then quarters are defined as 13-week
+periods (the final quarter of a 53-week year has 14-weeks).
+Otherwise, quarters are defined as 3-month periods.
+
+Additionally, ~:tree-type~ can be a function, in which case it should
+take the date as an argument, and generate a list of pairs for
+~org-datetree-find-create-hierarchy~. This allows for creating new
+types of datetrees (e.g. for lunar calendars, academic calendars,
+retail 4-4-5 calendars, etc).
+
** New and changed options
# Changes deadling with changing default values of customizations,
@@ -281,6 +305,14 @@ leave extra prompts after evaluation, and skipping the prompt
filtering can be more robust for such languages (as this avoids
removing false positive prompts).
+*** Elisp functions for new datetree tree-types
+
+Accompanying the [[#9.8-datetree-treetype][new datetree capture ~:tree-type~ options]], on the
+elisp level ~org-datetree-find-create-entry~ and
+~org-datetree-find-create-hierarchy~ generalize
+~org-datetree-find-date-create~, ~org-datetree-find-month-create~, and
+~org-datetree-find-iso-week-create~ to new datetree types.
+
** Removed or renamed functions and variables
*** ~org-cycle-display-inline-images~ is renamed to ~org-cycle-display-link-previews~
@@ -299,6 +331,13 @@ previews of supported link types besides image links.
The behavior is unchanged, except in that the new variable now affects
previews of supported link types besides image links.
+*** Obsolete functions and variables removed from ~org-datetree~
+
+Due to the refactoring of ~org-datetree~ to support the [[#9.8-datetree-treetype][new datetree
+capture ~:tree-type~ options]], the internal variable
+~org-datetree-base-level~ has been removed, as well as the
+undocumented helper function ~org-datetree-insert-line~.
+
** Miscellaneous
*** Org mode no longer prevents =flyspell= from spell-checking inside =LOGBOOK= drawers
diff --git a/lisp/org-capture.el b/lisp/org-capture.el
index 486304df2..818ed179b 100644
--- a/lisp/org-capture.el
+++ b/lisp/org-capture.el
@@ -59,6 +59,7 @@ (declare-function org-at-table-p "org-table" (&optional table-type))
(declare-function org-clock-update-mode-line "org-clock" (&optional refresh))
(declare-function org-datetree-find-date-create "org-datetree" (date &optional keep-restriction))
(declare-function org-datetree-find-month-create "org-datetree" (d &optional keep-restriction))
+(declare-function org-datetree-find-create-hierarchy "org-datetree" (hier-pairs &optional keep-restriction legacy-prop))
(declare-function org-decrypt-entry "org-crypt" ())
(declare-function org-element-at-point "org-element" (&optional pom cached-only))
(declare-function org-element-lineage "org-element-ast" (datum &optional types with-self))
@@ -293,7 +294,13 @@ (defcustom org-capture-templates nil
:tree-type When `week', make a week tree instead of the month-day
tree. When `month', make a month tree instead of the
- month-day tree.
+ month-day tree. When any subset of
+ `(year quarter month week day)', create a
+ datetree hierarchy with the specified
+ levels. Can also be a function, in which
+ case it should take the date as an argument
+ and generate a list of pairs to pass to
+ `org-datetree-find-create-hierarchy'.
:unnarrowed Do not narrow the target buffer, simply show the
full buffer. Default is to narrow it so that you
@@ -1090,10 +1097,22 @@ (defun org-capture-set-target-location (&optional target)
;; yesterday, if we are extending dates for a couple of
;; hours)
(funcall
- (pcase (org-capture-get :tree-type)
- (`week #'org-datetree-find-iso-week-create)
+ (pcase (org-capture-get :tree-type)
+ (`week #'org-datetree-find-iso-week-create)
(`month #'org-datetree-find-month-create)
- (_ #'org-datetree-find-date-create))
+ (`day #'org-datetree-find-date-create)
+ ((pred not) #'org-datetree-find-date-create)
+ ;; NOTE function case needs to be before list case to
+ ;; handle lambda forms correctly
+ ((and (pred functionp) fun)
+ (lambda (d keep-restriction)
+ (org-datetree-find-create-hierarchy
+ (funcall fun d) keep-restriction)))
+ ((and (pred listp) grouping)
+ (lambda (d keep-restriction)
+ (funcall #'org-datetree-find-create-entry grouping
+ d keep-restriction)))
+ (_ (error "Unrecognized :tree-type")))
(calendar-gregorian-from-absolute
(cond
(org-overriding-default-time
diff --git a/lisp/org-datetree.el b/lisp/org-datetree.el
index d0cc1fabb..4da3c8dc8 100644
--- a/lisp/org-datetree.el
+++ b/lisp/org-datetree.el
@@ -24,23 +24,20 @@
;;
;;; Commentary:
-;; This file contains code to create entries in a tree where the top-level
-;; nodes represent years, the level 2 nodes represent the months, and the
-;; level 1 entries days.
+;; This file contains code to create entries in a tree where the
+;; top-level nodes represent years, the level 2 nodes represent the
+;; months, and the level 1 entries days. It also implements
+;; extensions to the datetree that allow for other levels such as
+;; quarters and weeks.
;;; Code:
(require 'org-macs)
(org-assert-version)
+(require 'cal-iso)
(require 'org)
-
-(defvar org-datetree-base-level 1
- "The level at which years should be placed in the date tree.
-This is normally one, but if the buffer has an entry with a
-DATE_TREE (or WEEK_TREE for ISO week entries) property (any
-value), the date tree will become a subtree under that entry, so
-the base level will be properly adjusted.")
+(require 'org-element)
(defcustom org-datetree-add-timestamp nil
"When non-nil, add a time stamp matching date of entry.
@@ -59,174 +56,264 @@ (defun org-datetree-find-date-create (d &optional keep-restriction)
When it is nil, the buffer will be widened to make sure an existing date
tree can be found. If it is the symbol `subtree-at-point', then the tree
will be built under the headline at point."
- (org-datetree--find-create-group d 'day keep-restriction))
+ (org-datetree-find-create-entry '(year month day) d keep-restriction))
;;;###autoload
(defun org-datetree-find-month-create (d &optional keep-restriction)
"Find or create a month entry for date D.
Compared to `org-datetree-find-date-create' this function creates
-entries grouped by month instead of days.
+entries grouped by year-month instead of year-month-day.
If KEEP-RESTRICTION is non-nil, do not widen the buffer.
When it is nil, the buffer will be widened to make sure an existing date
tree can be found. If it is the symbol `subtree-at-point', then the tree
will be built under the headline at point."
- (org-datetree--find-create-group d 'month keep-restriction))
-
-(defun org-datetree--find-create-group
- (d time-grouping &optional keep-restriction)
- "Find or create an entry for date D.
-If time-period is day, group entries by day.
-If time-period is month, then group entries by month."
- (setq-local org-datetree-base-level 1)
- (save-restriction
- (if (eq keep-restriction 'subtree-at-point)
- (progn
- (unless (org-at-heading-p) (error "Not at heading"))
- (widen)
- (org-narrow-to-subtree)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1)))
- (unless keep-restriction (widen))
- ;; Support the old way of tree placement, using a property
- (let ((prop (org-find-property "DATE_TREE")))
- (when prop
- (goto-char prop)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1))
- (org-narrow-to-subtree))))
- (goto-char (point-min))
- (let ((year (calendar-extract-year d))
- (month (calendar-extract-month d))
- (day (calendar-extract-day d)))
- (org-datetree--find-create
- "\\([12][0-9]\\{3\\}\\)"
- year nil nil nil t)
- (org-datetree--find-create
- "%d-\\([01][0-9]\\) \\w+"
- year month nil nil t)
- (when (eq time-grouping 'day)
- (org-datetree--find-create
- "%d-%02d-\\([0123][0-9]\\) \\w+"
- year month day nil t)))))
+ (org-datetree-find-create-entry '(year month) d keep-restriction))
;;;###autoload
(defun org-datetree-find-iso-week-create (d &optional keep-restriction)
"Find or create an ISO week entry for date D.
Compared to `org-datetree-find-date-create' this function creates
-entries ordered by week instead of months.
-When it is nil, the buffer will be widened to make sure an existing date
-tree can be found. If it is the symbol `subtree-at-point', then the tree
-will be built under the headline at point."
- (setq-local org-datetree-base-level 1)
- (save-restriction
- (if (eq keep-restriction 'subtree-at-point)
- (progn
- (unless (org-at-heading-p) (error "Not at heading"))
- (widen)
- (org-narrow-to-subtree)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1)))
- (unless keep-restriction (widen))
- ;; Support the old way of tree placement, using a property
- (let ((prop (org-find-property "WEEK_TREE")))
- (when prop
- (goto-char prop)
- (setq-local org-datetree-base-level
- (org-get-valid-level (org-current-level) 1))
- (org-narrow-to-subtree))))
- (goto-char (point-min))
- (require 'cal-iso)
- (let* ((year (calendar-extract-year d))
- (month (calendar-extract-month d))
- (day (calendar-extract-day d))
- (time (org-encode-time 0 0 0 day month year))
- (iso-date (calendar-iso-from-absolute
- (calendar-absolute-from-gregorian d)))
- (weekyear (nth 2 iso-date))
- (week (nth 0 iso-date)))
- ;; ISO 8601 week format is %G-W%V(-%u)
- (org-datetree--find-create
- "\\([12][0-9]\\{3\\}\\)"
- weekyear nil nil (format-time-string "%G" time) t)
- (org-datetree--find-create
- "%d-W\\([0-5][0-9]\\)"
- weekyear week nil (format-time-string "%G-W%V" time) t)
- ;; For the actual day we use the regular date instead of ISO week.
- (org-datetree--find-create
- "%d-%02d-\\([0123][0-9]\\) \\w+" year month day nil t))))
+entries grouped by year-week-day instead of year-month-day. If
+KEEP-RESTRICTION is non-nil, do not widen the buffer. When it is
+nil, the buffer will be widened to make sure an existing date
+tree can be found. If it is the symbol `subtree-at-point', then
+the tree will be built under the headline at point."
+ (org-datetree-find-create-entry '(year week day) d keep-restriction))
-(defun org-datetree--find-create
- (regex-template year &optional month day insert match-title)
- "Find the datetree matched by REGEX-TEMPLATE for YEAR, MONTH, or DAY.
-REGEX-TEMPLATE is passed to `format' with YEAR, MONTH, and DAY as
-arguments.
+;;;###autoload
+(defun org-datetree-find-create-entry
+ (time-grouping d &optional keep-restriction)
+ "Find or create an entry for date D.
+Moves point to the beginning of the entry.
-If MATCH-TITLE is non-nil, REGEX-TEMPLATE is matched against
-heading title and the exact regexp matched against heading line is:
+TIME-GROUPING specifies the grouping levels of the datetree, and
+should be a subset of `(year quarter month week day)'. Weeks are
+assigned to years according to ISO-8601. If TIME-GROUPING
+contains both `month' and `week', then weeks are assigned to the
+month containing Thursday, for consistency with the ISO-8601
+year-week rule. If TIME-GROUPING contains `quarter' and `week'
+but not `month', quarters are defined as 13-week periods;
+otherwise they are defined as 3-month periods.
- (format org-complex-heading-regexp-format
- (format regex-template year month day))
+If KEEP-RESTRICTION is non-nil, do not widen the buffer. When it
+is nil, the buffer will be widened to make sure an existing date
+tree can be found. If it is the symbol `subtree-at-point', then
+the tree will be built under the headline at point.
-If MATCH-TITLE is nil, the regexp matched against heading line is
-REGEX-TEMPLATE:
+If `org-datetree-add-timestamp' is non-nil and TIME-GROUPING
+includes `day' and a new entry is created, adds a time stamp
+after the new headline."
+ (when-let ((setdiff (seq-difference time-grouping
+ '(year quarter month week day))))
+ (error (format "Unrecognized datetree grouping elements %s" setdiff)))
+ (let* ((year (calendar-extract-year d))
+ (month (calendar-extract-month d))
+ (day (calendar-extract-day d))
+ (time (org-encode-time 0 0 0 day month year))
+ (iso-date (calendar-iso-from-absolute
+ (calendar-absolute-from-gregorian d)))
+ (week (nth 0 iso-date))
+ (nominal-year
+ (if (memq 'week time-grouping)
+ (nth 2 iso-date)
+ year))
+ (nominal-month
+ (if (memq 'week time-grouping)
+ (calendar-extract-month
+ ;; anchor on Thurs, to be consistent with weekyear
+ (calendar-gregorian-from-absolute
+ (calendar-iso-to-absolute
+ `(,week 4 ,nominal-year))))
+ month))
+ (quarter (if (and (memq 'week time-grouping)
+ (not (memq 'month time-grouping)))
+ (min 4 (1+ (/ (1- week) 13)))
+ (1+ (/ (1- nominal-month) 3))))
+ (found-p
+ (org-datetree-find-create-hierarchy
+ (append
+ (when (memq 'year time-grouping)
+ (list (list (number-to-string nominal-year)
+ (org-datetree-comparefun-from-regex
+ "\\([12][0-9]\\{3\\}\\)"))))
+ (when (memq 'quarter time-grouping)
+ (list (list (format "%d-Q%d" nominal-year quarter)
+ (org-datetree-comparefun-from-regex
+ "\\([12][0-9]\\{3\\}-Q[1-4]\\)"))))
+ (when (memq 'month time-grouping)
+ (list (list (format-time-string
+ "%Y-%m %B" (org-encode-time 0 0 0 1 nominal-month
+ nominal-year))
+ (org-datetree-comparefun-from-regex
+ "\\([12][0-9]\\{3\\}-[01][0-9]\\) \\w+"))))
+ (when (memq 'week time-grouping)
+ (list (list (format-time-string "%G-W%V" time)
+ (org-datetree-comparefun-from-regex
+ "\\([12][0-9]\\{3\\}-W[0-5][0-9]\\)"))))
+ (when (memq 'day time-grouping)
+ ;; Use regular date instead of ISO-week year/month
+ (list (list (format-time-string
+ "%Y-%m-%d %A" (org-encode-time 0 0 0 day month year))
+ (org-datetree-comparefun-from-regex
+ "\\([12][0-9]\\{3\\}-[01][0-9]-[0123][0-9]\\) \\w+")))))
+ keep-restriction
+ ;; Support the old way of tree placement, using a property
+ (cond
+ ((seq-set-equal-p time-grouping '(year month day))
+ "DATE_TREE")
+ ((seq-set-equal-p time-grouping '(year month))
+ "DATE_TREE")
+ ((seq-set-equal-p time-grouping '(year week day))
+ "WEEK_TREE")))))
+ (when (memq 'day time-grouping)
+ (when (and (not found-p) org-datetree-add-timestamp)
+ (save-excursion
+ (end-of-line)
+ (insert "\n")
+ (org-indent-line)
+ (org-insert-timestamp
+ (org-encode-time 0 0 0 day month year)
+ nil
+ (eq org-datetree-add-timestamp 'inactive)))))))
- (format regex-template year month day)
+(defun org-datetree-comparefun-from-regex (sibling-regex)
+ "Construct comparison function based on regular expression.
+The generated comparison function can be used with
+`org-datetree-find-create-hierarchy'. SIBLING-REGEX should be a
+regex that matches the headline and its siblings, with 1 match
+group. Headlines are compared by the lexicographic ordering of
+match group 1. The generated function returns -1 if the first
+argument is earlier, 1 if later, 0 if equal, or nil if either
+argument doesn't match."
+ (lambda (sibling-title new-title)
+ (let ((target-match (and (string-match sibling-regex new-title)
+ (match-string 1 new-title)))
+ (sibling-match (and (string-match sibling-regex sibling-title)
+ (match-string 1 sibling-title))))
+ (cond
+ ((not (and target-match sibling-match)) nil)
+ ((string< sibling-match target-match) -1)
+ ((string> sibling-match target-match) 1)
+ (t 0)))))
-Match group 1 in REGEX-TEMPLATE is compared against the specified date
-component. If INSERT is non-nil and there is no match then it is
-inserted into the buffer."
- (when (or month day)
- (org-narrow-to-subtree))
- ;; ensure that the first match group in REGEX-TEMPLATE
- ;; is the first inside `org-complex-heading-regexp-format'
- (when (and match-title
- (not (string-match-p "\\\\(\\?1:" regex-template))
- (string-match "\\\\(" regex-template))
- (setq regex-template (replace-match "\\(?1:" nil t regex-template)))
- (let ((re (if match-title
- (format org-complex-heading-regexp-format
- (format regex-template year month day))
- (format regex-template year month day)))
- match)
- (goto-char (point-min))
- (while (and (setq match (re-search-forward re nil t))
- (goto-char (match-beginning 1))
- (< (string-to-number (match-string 1)) (or day month year))))
- (cond
- ((not match)
- (goto-char (point-max))
- (unless (bolp) (insert "\n"))
- (org-datetree-insert-line year month day insert))
- ((= (string-to-number (match-string 1)) (or day month year))
- (forward-line 0))
- (t
- (forward-line 0)
- (org-datetree-insert-line year month day insert)))))
+(defun org-datetree-find-create-hierarchy
+ (hier-pairs &optional keep-restriction legacy-prop)
+ "Find or create entry in datetree using the full date hierarchy.
+Moves point to the beginning of the entry. Returns non-nil if an
+existing entry was found, or nil if a new entry was created.
+
+HIER-PAIRS is a list whose first entry corresponds to the outermost element
+(e.g. year) and last entry corresponds to the innermost (e.g. day).
+Each entry of the list is a pair, the car is the headline for that level
+(e.g. \"2024\" or \"2024-12-28 Saturday\"), and the cadr is a
+string comparison function for sorting each headline among its
+siblings. The comparison function should take 2 arguments,
+corresponding to the titles of 2 headlines, and return a negative
+number if the first headline is earlier, a positive number if the
+second headline is earlier, 0 or t if the headlines are at the
+same time, or `nil' if a headline isn't a valid datetree
+subheading. For example, HIER-PAIRS could look like
-(defun org-datetree-insert-line (year &optional month day text)
- (delete-region (save-excursion (skip-chars-backward " \t\n") (point)) (point))
- (when (org--blank-before-heading-p) (insert "\n"))
- (insert "\n" (make-string org-datetree-base-level ?*) " \n")
- (backward-char)
- (when month (org-do-demote))
- (when day (org-do-demote))
- (if text
- (insert text)
- (insert (format "%d" year))
- (when month
+ ((\"2024\" compare-year-fun)
+ (\"2024-12 December\" compare-month-fun)
+ (\"2024-12-28 Saturday\" compare-day-fun))
+
+where compare-month-fun would be some function where
+(compare-month-fun \"2024-11 November\" \"2024-12 December\") is
+negative, and (compare-month-fun \"2024-12-December\" \"Potato\")
+is nil. One way to construct such a comparison function is with
+`org-datetree-comparefun-from-regex'.
+
+If KEEP-RESTRICTION is non-nil, do not widen the buffer.
+When it is nil, the buffer will be widened to make sure an existing date
+tree can be found. If it is the symbol `subtree-at-point', then the tree
+will be built under the headline at point.
+
+If LEGACY-PROP is non-nil, the tree is located by searching for a
+headline with property LEGACY-PROP, supporting the old way of
+tree placement via a property."
+ (let ((level 1)
+ found-p)
+ (save-restriction
+ ;; get the datetree base and narrow to it
+ (if (eq keep-restriction 'subtree-at-point)
+ (progn
+ (unless (org-at-heading-p) (error "Not at heading"))
+ (widen)
+ (org-narrow-to-subtree)
+ (setq level (org-get-valid-level (org-current-level) 1)))
+ (unless keep-restriction (widen))
+ ;; Support the old way of tree placement, using a property
+ (let ((prop (and legacy-prop (org-find-property legacy-prop))))
+ (when prop
+ (progn
+ (goto-char prop)
+ (org-narrow-to-subtree)
+ (setq level (org-get-valid-level (org-current-level) 1))))))
+ (cl-loop
+ for pair in hier-pairs
+ do
+ (setq found-p (org-datetree--find-create-subheading
+ (cadr pair) (car pair) level))
+ (setq level (1+ level))))
+ found-p))
+
+(defun org-datetree--find-create-subheading
+ (compare-fun new-title level)
+ "Find datetree subheading, or create it if it doesn't exist.
+After insertion, move point to beginning of the subheading, and
+narrow to its subtree. Returns non-nil if the heading was found,
+or nil if a new heading was created.
+
+NEW-TITLE is the title of the subheading to be found or created.
+LEVEL is the level of the headline to be found or created.
+COMPARE-FUN is a function of 2 arguments for comparing headline
+titles; it should return a negative number if the first headline
+precedes the second, a positive number if the second number has
+precedence, 0 or t if the headlines are at the same time, and nil
+if a headline isn't a valid datetree subheading at this level."
+ (let* ((nstars (if org-odd-levels-only (1- (* 2 level)) level))
+ (heading-re (format "^\\*\\{%d\\}" nstars))
+ (sibling (car (org-element-cache-map
+ (lambda (d)
+ (when (= (org-element-property :level d) level)
+ (let ((compare-result
+ (funcall compare-fun
+ (org-element-property :raw-value d)
+ new-title)))
+ (and compare-result
+ (or (eq compare-result t) (>= compare-result 0))
+ d))))
+ :granularity 'headline
+ :restrict-elements '(headline)
+ :next-re heading-re
+ :fail-re heading-re
+ :narrow t
+ :limit-count 1))))
+ ;; go to headline, or first successor sibling, or end of buffer
+ (if sibling
+ (goto-char (org-element-property :begin sibling))
+ (goto-char (point-max))
+ (unless (bolp) (insert "\n")))
+ (if (and sibling
+ (memq (funcall compare-fun
+ (org-element-property :raw-value sibling)
+ new-title)
+ '(0 t)))
+ ;; narrow and return the matched headline
+ (progn
+ (org-narrow-to-subtree)
+ t)
+ ;; insert new headline, narrow, and return it
+ (delete-region (save-excursion (skip-chars-backward " \t\n") (point)) (point))
+ (when (org--blank-before-heading-p) (insert "\n"))
(insert
- (if day
- (format-time-string "-%m-%d %A" (org-encode-time 0 0 0 day month year))
- (format-time-string "-%m %B" (org-encode-time 0 0 0 1 month year))))))
- (when (and day org-datetree-add-timestamp)
- (save-excursion
- (insert "\n")
- (org-indent-line)
- (org-insert-timestamp
- (org-encode-time 0 0 0 day month year)
- nil
- (eq org-datetree-add-timestamp 'inactive))))
- (forward-line 0))
+ (format "\n%s %s\n"
+ (make-string nstars ?*)
+ new-title))
+ (forward-line -1)
+ (org-narrow-to-subtree)
+ nil)))
(defun org-datetree-file-entry-under (txt d)
"Insert a node TXT into the date tree under date D."
diff --git a/testing/lisp/test-org-capture.el b/testing/lisp/test-org-capture.el
index 4aed0e99e..ff7e242d2 100644
--- a/testing/lisp/test-org-capture.el
+++ b/testing/lisp/test-org-capture.el
@@ -324,6 +324,51 @@ (ert-deftest test-org-capture/entry ()
(insert "Capture text")
(org-capture-finalize)))
(buffer-string))))
+ ;; test datetree capture with list tree-type
+ (should
+ (equal
+ "* A\n** B\n*** 2024\n**** 2024-Q2\n***** 2024-06 June\n****** 2024-06-16 Sunday\n******* H1 Capture text\n** C\n"
+ (org-test-with-temp-text-in-file "* A\n** B\n** C\n"
+ (let* ((file (buffer-file-name))
+ (org-capture-templates
+ `(("t"
+ "Todo"
+ entry
+ (file+olp+datetree ,file (lambda ()
+ (should (equal ,file (buffer-file-name)))
+ '("A" "B")))
+ "* H1 %?"
+ :tree-type
+ (year quarter month day)))))
+ (org-test-at-time "2024-06-16"
+ (org-capture nil "t")
+ (insert "Capture text")
+ (org-capture-finalize)))
+ (buffer-string))))
+ ;; test datetree capture with function tree-type
+ (should
+ (equal
+ "* A\n** B\n*** 2024\n**** 06\n***** 16\n****** H1 Capture text\n** C\n"
+ (org-test-with-temp-text-in-file "* A\n** B\n** C\n"
+ (let* ((file (buffer-file-name))
+ (org-capture-templates
+ `(("t"
+ "Todo"
+ entry
+ (file+olp+datetree ,file (lambda ()
+ (should (equal ,file (buffer-file-name)))
+ '("A" "B")))
+ "* H1 %?"
+ :tree-type
+ (lambda (d)
+ `((,(format "%d" (calendar-extract-year d)) compare-strings)
+ (,(format "%02d" (calendar-extract-month d)) compare-strings)
+ (,(format "%02d" (calendar-extract-day d)) compare-strings)))))))
+ (org-test-at-time "2024-06-16"
+ (org-capture nil "t")
+ (insert "Capture text")
+ (org-capture-finalize)))
+ (buffer-string))))
(should
(equal
"* A\n** B\n*** 2024\n**** 2024-06 June\n***** 2024-06-16 Sunday\n****** H1 Capture text\n** C\n"
diff --git a/testing/lisp/test-org-datetree.el b/testing/lisp/test-org-datetree.el
index 620a916df..d6ba32887 100644
--- a/testing/lisp/test-org-datetree.el
+++ b/testing/lisp/test-org-datetree.el
@@ -91,6 +91,15 @@ (ert-deftest test-org-datetree/find-date-create ()
(let ((org-datetree-add-timestamp 'inactive))
(org-datetree-find-date-create '(3 29 2012)))
(org-trim (buffer-string)))))
+ ;; don't add the timestamp twice
+ (should
+ (string-match
+ "\\`\\* 2012\n\n\\*\\* 2012-03 .*\n\n\\*\\*\\* \\(2012-03-29\\) .*\n[ \t]*<\\1.*?>\\'"
+ (org-test-with-temp-text "* 2012\n"
+ (let ((org-datetree-add-timestamp t))
+ (org-datetree-find-date-create '(3 29 2012))
+ (org-datetree-find-date-create '(3 29 2012)))
+ (org-trim (buffer-string)))))
;; Insert at top level, unless some node has DATE_TREE property. In
;; this case, date tree becomes one of its sub-trees.
(should
@@ -108,6 +117,15 @@ (ert-deftest test-org-datetree/find-date-create ()
(let ((org-datetree-add-timestamp nil))
(org-datetree-find-date-create '(3 29 2012)))
(org-trim (buffer-string)))))
+ ;; Do not create new year/month node in DATE_TREE when it already exists
+ (should
+ (string-match
+ "\\`\\* H1\n\n\\*\\* H1.1\n:PROPERTIES:\n:DATE_TREE: t\n:END:\n\n\\*\\*\\* 2012\n\n\\*\\*\\*\\* 2012-03 month\n\n\\*\\*\\*\\*\\* 2012-03-29 .*\n\n\\* H2\\'"
+ (org-test-with-temp-text
+ "* H1\n\n** H1.1\n:PROPERTIES:\n:DATE_TREE: t\n:END:\n\n*** 2012\n\n**** 2012-03 month\n\n* H2"
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-date-create '(3 29 2012)))
+ (org-trim (buffer-string)))))
;; Insert at correct location, even if some other heading has a
;; subtree that looks like a datetree
(should
@@ -155,9 +173,83 @@ (ert-deftest test-org-datetree/find-month-create ()
(should
(string-match
"\\`\\* 2012\n\n\\*\\* 2012-03 .*\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-month-create '(3 29 2012)))
+ (org-trim (buffer-string)))))
+ ;; Insert at top level, unless some node has DATE_TREE property. In
+ ;; this case, date tree becomes one of its sub-trees.
+ (should
+ (string-match
+ "\\* 2012"
+ (org-test-with-temp-text "* Top"
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-month-create '(3 29 2012)))
+ (org-trim (buffer-string)))))
+ (should
+ (string-match
+ "\\*\\* H1.1\n:PROPERTIES:\n:DATE_TREE: t\n:END:\n\n\\*\\*\\* 2012"
+ (org-test-with-temp-text
+ "* H1\n\n** H1.1\n:PROPERTIES:\n:DATE_TREE: t\n:END:\n\n* H2"
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-month-create '(3 29 2012)))
+ (org-trim (buffer-string)))))
+ ;; Do not create new year/month node in DATE_TREE when it already exists
+ (should
+ (string-match
+ "\\`\\* H1\n\n\\*\\* H1.1\n:PROPERTIES:\n:DATE_TREE: t\n:END:\n\n\\*\\*\\* 2012\n\n\\*\\*\\*\\* 2012-03 month\n\n\\* H2\\'"
+ (org-test-with-temp-text
+ "* H1\n\n** H1.1\n:PROPERTIES:\n:DATE_TREE: t\n:END:\n\n*** 2012\n\n**** 2012-03 month\n\n* H2"
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-month-create '(3 29 2012)))
+ (org-trim (buffer-string)))))))
+
+(ert-deftest test-org-datetree/find-quarter-month-create ()
+ "Test `org-datetree-find-quarter-month-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2012\n\n\\*\\* 2012-Q1\n\n\\*\\*\\* 2012-03 .*\\'"
(org-test-with-temp-text ""
(let ((org-datetree-add-timestamp nil))
- (org-datetree-find-month-create '(3 29 2012)))
+ (org-datetree-find-create-entry '(year quarter month) '(3 29 2012)))
+ (org-trim (buffer-string)))))))
+
+(ert-deftest test-org-datetree/find-quarter-month-day-create ()
+ "Test `org-datetree-find-quarter-month-day-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2012\n\n\\*\\* 2012-Q1\n\n\\*\\*\\* 2012-03 .*\n\n\\*\\*\\*\\* 2012-03-29 .*\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-create-entry '(year quarter month day) '(3 29 2012)))
+ (org-trim (buffer-string)))))))
+
+(ert-deftest test-org-datetree/find-quarter-week-create ()
+ "Test `org-datetree-find-quarter-week-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2024\n\n\\*\\* 2024-Q4\n\n\\*\\*\\* 2024-W52\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-create-entry '(year quarter week) '(12 27 2024)))
+ (org-trim (buffer-string)))))))
+
+(ert-deftest test-org-datetree/find-month-week-create ()
+ "Test `org-datetree-find-month-week-create' specifications."
+ (let ((org-blank-before-new-entry '((heading . t))))
+ ;; When date is missing, create it with the entry under month.
+ (should
+ (string-match
+ "\\`\\* 2024\n\n\\*\\* 2024-12 .*\n\n\\*\\*\\* 2024-W52\\'"
+ (org-test-with-temp-text ""
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-create-entry '(year month week) '(12 27 2024)))
(org-trim (buffer-string)))))))
(ert-deftest test-org-datetree/find-iso-week-create ()
@@ -260,6 +352,15 @@ (ert-deftest test-org-datetree/find-iso-week-create ()
(let ((org-datetree-add-timestamp nil))
(org-datetree-find-iso-week-create '(12 31 2014)))
(org-trim (buffer-string)))))
+ ;; Do not create new year/week node when it exists in WEEK_TREE
+ (should
+ (string-match
+ "\\`\\* H1\n\\*\\* H1.1\n:PROPERTIES:\n:WEEK_TREE: t\n:END:\n\n\\*\\*\\* 2015\n\n\\*\\*\\*\\* 2015-W01\n\n\\*\\*\\*\\*\\* 2014-12-31 .*\n\n\\* H2\\'"
+ (org-test-with-temp-text
+ "* H1\n** H1.1\n:PROPERTIES:\n:WEEK_TREE: t\n:END:\n\n*** 2015\n\n**** 2015-W01\n\n* H2"
+ (let ((org-datetree-add-timestamp nil))
+ (org-datetree-find-iso-week-create '(12 31 2014)))
+ (org-trim (buffer-string)))))
;; Always leave point at beginning of day entry.
(should
(string-match
--
2.47.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: Month-week and quarter-week datetrees (RFC and package announcement)
2024-12-31 1:56 ` Jack Kamm
@ 2025-01-01 9:14 ` Ihor Radchenko
0 siblings, 0 replies; 11+ messages in thread
From: Ihor Radchenko @ 2025-01-01 9:14 UTC (permalink / raw)
To: Jack Kamm; +Cc: emacs-orgmode
Jack Kamm <jackkamm@gmail.com> writes:
> Thanks for the feedback. I attach a squashed updated patch for part 2.
> You can also see the unsquashed changes at https://github.com/jackkamm/org-mode/tree/2024-grouped-weektree-rebase
> ...
I have no further comments.
Feel free to push.
--
Ihor Radchenko // yantar92,
Org mode maintainer,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2025-01-01 9:14 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2023-12-30 19:41 Month-week and quarter-week datetrees (RFC and package announcement) Jack Kamm
2023-12-31 14:50 ` Ihor Radchenko
2023-12-31 18:16 ` Jack Kamm
2024-12-16 18:49 ` Ihor Radchenko
2024-12-28 6:09 ` Jack Kamm
2024-12-29 9:18 ` Jack Kamm
2024-12-29 10:33 ` Ihor Radchenko
2024-12-30 16:20 ` Jack Kamm
2024-12-30 17:11 ` Ihor Radchenko
2024-12-31 1:56 ` Jack Kamm
2025-01-01 9:14 ` Ihor Radchenko
Code repositories for project(s) associated with this external index
https://git.savannah.gnu.org/cgit/emacs.git
https://git.savannah.gnu.org/cgit/emacs/org-mode.git
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.