├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .travis.yml ├── Keg ├── LICENSE ├── Makefile ├── README.md ├── README.org ├── org-listcruncher.el └── test ├── test-org-listcruncher.el └── test-org-listcruncher.org /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | continue-on-error: 9 | ${{ contains(fromJson('["snapshot"]'), matrix.emacs_version) }} 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | emacs_version: 14 | - '27.2' 15 | - '28.1' 16 | - '29.1' 17 | - '30.1' 18 | - 'snapshot' 19 | steps: 20 | - uses: actions/checkout@v1 21 | - uses: purcell/setup-emacs@master 22 | with: 23 | version: ${{ matrix.emacs_version }} 24 | - uses: conao3/setup-keg@master 25 | - run: make test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled 2 | *.elc 3 | 4 | # Packaging 5 | .keg 6 | 7 | # Backup files 8 | *~ 9 | 10 | # Undo-tree save-files 11 | *.~undo-tree 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: emacs-lisp 2 | 3 | sudo: required 4 | 5 | services: 6 | - docker 7 | 8 | before_install: 9 | - docker pull silex/emacs:27-ci-cask 10 | 11 | script: 12 | - ls -l 13 | - docker run silex/emacs:27-ci-cask /bin/bash -c "emacs --version" 14 | - docker run silex/emacs:27-ci-cask /bin/bash -c "cask --version" 15 | - docker run --mount type=bind,source="$(pwd)",target=/build silex/emacs:27-ci-cask /bin/bash -c "cd build; make test" 16 | 17 | -------------------------------------------------------------------------------- /Keg: -------------------------------------------------------------------------------- 1 | (source gnu melpa 2 | (nongnu . "https://elpa.nongnu.org/nongnu/")) 3 | (package 4 | (org-listcruncher 5 | (recipe . (keg :fetcher github :repo "dfeich/org-listcruncher")))) 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EMACS ?= emacs 2 | 3 | # only use KEG if it is available in the PATH or the location is explicitely 4 | # given 5 | ifndef KEG 6 | ifneq (, $(shell which keg)) 7 | KEGEXEC = keg exec 8 | KEG = keg 9 | endif 10 | else 11 | KEGEXEC = $(KEG) exec 12 | endif 13 | 14 | ifdef TESTNAME 15 | testcmd = (ert \"$(TESTNAME)\") 16 | else 17 | testcmd = (ert t) 18 | endif 19 | 20 | .PHONY: test debug clean 21 | 22 | .SUFFIXES: .el .elc 23 | .el.elc: 24 | $(EMACS) -Q --batch -L . -f batch-byte-compile $< 25 | 26 | 27 | all: test 28 | 29 | compile: org-listcruncher.elc 30 | @echo "Byte compiling $<" 31 | $(KEGEXEC) $(EMACS) --batch \ 32 | --eval "(byte-compile-file \"org-listcruncher.el\")" 33 | 34 | test: org-listcruncher.elc 35 | @echo "Emacs binary at $(shell which $(EMACS))" 36 | $(KEGEXEC) $(EMACS) --batch \ 37 | -l org-listcruncher.elc \ 38 | -l test/test-org-listcruncher.el \ 39 | --eval "(princ (format \"Emacs version: %s\n\" (emacs-version)) t)" \ 40 | --eval "(princ (format \"Org version: %s\n\" (org-version)) t)" \ 41 | --eval "(ert-run-tests-batch-and-exit test-order)" 42 | 43 | debug: 44 | $(KEGEXEC) $(EMACS) -q \ 45 | -l org-listcruncher.el \ 46 | -l test/test-org-listcruncher.el \ 47 | --eval "(progn (setq occ-no-cleanup t)$(testcmd))" 48 | 49 | clean: 50 | rm -f org-listcruncher.elc 51 | @if test x"${KEGEXEC}" != x; then \ 52 | $(KEG) clean; \ 53 | fi 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Table of Contents 3 | 4 | 1. [Org listcruncher](#orge2e4695) 5 | 1. [Installation](#orgea393ec) 6 | 2. [Example usage](#org59c78c5) 7 | 3. [List writing rules](#org7196933) 8 | 4. [Using alternate parsing functions](#org884e7e3) 9 | 5. [Configuration](#org2896421) 10 | 6. [Using org table spreadsheet formulas to finalize the result](#org5a2e82c) 11 | 7. [Changes](#org22c4939) 12 | 1. [version 1.0: API change](#orgdaf2a50) 13 | 2. [version 1.2: change for using operator values](#org9399d06) 14 | 3. [version 1.4: introduction of the :formula feature](#orgc958900) 15 | 8. [Running tests](#org10d1dfc) 16 | 17 | 18 | 19 | 20 | # Org listcruncher 21 | 22 | [![CI workflow](https://github.com/dfeich/org-listcruncher/actions/workflows/CI.yml/badge.svg)](https://github.com/dfeich/org-listcruncher/actions) 23 | [![img](https://melpa.org/packages/org-listcruncher-badge.svg)](https://melpa.org/#/org-listcruncher) 24 | 25 | Org-listcruncher is a planning tool. Planning using lists is a very 26 | natural approach, and in terms of a data structure it is similar to 27 | a mind map. Emacs Org-mode makes it very easy and efficient to work 28 | with lists, since it offers a lot of functionality for restructuring 29 | and modifying them. 30 | 31 | Org-listcruncher provides a way to convert such Org-mode lists into 32 | a table structure following specific semantics. This tabular data 33 | structure can then be operated on by other code blocks, or it can 34 | just be exported. The list can retain all the comments, you can 35 | override values and keep the history of changes inside of it, while 36 | still being able to derive an easy data structure as an input for 37 | other planning tools further downstream. 38 | 39 | I've used it for the initial stage of planning for bigger projects. 40 | It was ideal for producing the often complex tables required by 41 | formal project proposal templates, and at some point I just exported 42 | the basic structures as a good starting point for continuing with a 43 | typical project management software. 44 | 45 | 46 | 47 | 48 | ## Installation 49 | 50 | You can get the package from [MELPA](https://melpa.org/#/org-listcruncher) using emacs' package manager. 51 | 52 | If you are using John Wiegley's `use-package` (which I recommend), just put the following line 53 | into your `~/emacs.d/init.el` (or `~/.emacs`) 54 | 55 | (use-package org-listcruncher) 56 | 57 | Or more barebones, just `require` it. 58 | 59 | (require 'org-listcruncher) 60 | 61 | 62 | 63 | 64 | ## Example usage 65 | 66 | Write a planning list and give it a name using the appropriate Org 67 | syntax (e.g. `#+NAME: lstTest`). Here is an example (look at the 68 | [raw form of this README.org](https://raw.githubusercontent.com/dfeich/org-listcruncher/master/README.org) to see the Org source buffer with all 69 | the markup) 70 | 71 | - **item:** item A modified by replacing values (amount: 15, responsible: Peter, end-year: 2020) 72 | - modification of item A (amount: 20) 73 | - another modification of item A introducing a column (newcol: 500) 74 | - modification of the modification (newcol: 299) 75 | - illustrating inheritance (responsible: Mary, end-year: 2024) 76 | - **item:** item B. Some longer explanation that may run over 77 | multiple lines (amount: 10) 78 | - **item:** item C (amount: 20) 79 | - **item:** item D (amount: 30, newcol: 35) 80 | - a modification to item C (amount: 25, responsible: Paul) 81 | - **item:** item X modified by operations (amount: 50, responsible: Peter, end-year: 2026) 82 | - modification by an operation (amount: +=50) 83 | - modification by an operation (amount: \*=1.5) 84 | - **item:** item Y entered in scientific format (amount: 1e3, responsible: Mary, end-year: 2025) 85 | - modification by an operation (amount: -=1e2) 86 | - **item:** item Z illustrating += and -= with strings (amount: 1000, responsible: Peter, end-year: 2025) 87 | - adding a string (responsible: +=Paul, end-year: +=1) 88 | - adding a string (responsible: +=Mary, end-year: +=1) 89 | - removing a string (responsible: -=Peter) 90 | 91 | We can use org-listcruncher to convert this list into a table 92 | 93 | (org-listcruncher-to-table listname) 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 |
descriptionnewcolamountresponsibleend-year
item A modified by replacing values29920Peter2020
item B 10Mary2024
item C 20Mary2024
item D3525Paul2024
item X modified by operations 150.0Peter2026
item Y entered in scientific format 900.0Mary2025
item Z illustrating += and -= with strings 1000Paul Mary2027
183 | 184 | We can also provide an additional argument to affect the order of 185 | columns in which the table is produced. 186 | 187 | (org-listcruncher-to-table listname :order '("description" "amount" "responsible")) 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 |
descriptionamountresponsiblenewcolend-year
item A modified by replacing values20Peter2992020
item B10Mary 2024
item C20Mary 2024
item D25Paul352024
item X modified by operations150.0Peter 2026
item Y entered in scientific format900.0Mary 2025
item Z illustrating += and -= with strings1000Paul Mary 2027
277 | 278 | It is also possible to directly obtain single table field values based on defining the 279 | row and column through the string corresponding to an item's description and its 280 | column name: 281 | 282 | (org-listcruncher-get-field listname "item B" "amount") 283 | 284 | 10 285 | 286 | 287 | 288 | 289 | ## List writing rules 290 | 291 | The rules for writing such a planning list are 292 | 293 | 1. Each line contains a tag defining whether the line will become a table row. For this 294 | example I defined this as the string "item:". Rows without such a tag just serve as 295 | metadata. 296 | 2. A string following the output tag "item:" is taken as the description of the table row. 297 | 3. Each line can contain any number of key/value pairs in parentheses in the form 298 | `(key1: val1, key2: val2, ...)` 299 | 4. Lines of lower hierarchical order in the list inherit their default settings for key/values 300 | from the upper items. 301 | 5. The key value of a higher order item can be overwritten by a new new value for the same key 302 | in a lower order line. 303 | 6. If a given value is of the form +=10, -=10, /=10, \*=10, i.e. an operator followed by a number, 304 | the operation is carried out on the previous value of the respective key. 305 | (Note: this changed in version 1.2, since the original use of "-10" did not 306 | allow differentiating between subtracting 10 or setting value to "-10". The 307 | old syntax is still allowed for all operators except "-") 308 | 7. If a given value is of the form +=word then "word" is 309 | added to the previous string value for this key, using space as a separator. If 310 | -=word is used, then "word" is removed from the previous string 311 | value. This allows building lists of words. 312 | 313 | 314 | 315 | 316 | ## Using alternate parsing functions 317 | 318 | You can define arbitrary parsing functions for the list items. They must 319 | obey the following API: 320 | 321 | The function receives a list item (a string) as its single 322 | argument. It must return a list (`OUTP, DESCR, VARLST`), where 323 | 324 | - `OUTP` is a boolean indicating whether this list item will become a table 325 | row 326 | - `DESCR` is the description string appearing in the table's "description" column 327 | (so this is only relevant for OUTP=True lines) 328 | - `VARLST` is the list of key/value pairs corresponding to the column name / 329 | values. 330 | 331 | Simple example functions for this purpose can be generated using 332 | the `org-listcruncher-mk-parseitem-default` generator function. It 333 | allows modifying the tag that decides whether a list item will 334 | become a table row. It also permits changing the description's 335 | terminating tag and the brackets for the key/value pairs. E.g. if I 336 | would like to match for "row:" instead for "item:", and if I would 337 | like to use square brackets, I can obtain such a function by 338 | executing. 339 | 340 | (org-listcruncher-mk-parseitem-default :tag"\\*?row:\\*?" :bra "[" :ket "]") 341 | 342 | Let's test it using this modified list: 343 | 344 | - **row:** item A modified by replacing values [amount: 15, recurrence: 1, end-year: 2020]. 345 | - modification of item A [amount: 20] 346 | - another modification of item A [newcol: 500] 347 | - modification of the modification [newcol: 299] 348 | - illustrating inheritance [recurrence: 2, end-year: 2024] 349 | - **row:** item B. Some longer explanation that may run over 350 | multiple lines [amount: 10] 351 | - **row:** item C [amount: 20] 352 | - **row:** item D [amount: 30] 353 | - a modification to item D [amount: 25, recurrence: 3] 354 | - **row:** item X modified by operations [amount: 50, recurrence: 4, end-year: 2026] 355 | - modification by an operation [amount: +50] 356 | - modification by an operation [amount: \*1.5] 357 | - **row:** item Y entered in scientific format [amount: 1e3, recurrence: 3, end-year: 2025] 358 | - modification by an operation [amount: -=1e2] 359 | 360 | We invoke org-listcruncher with the above parsing function: 361 | 362 | (org-listcruncher-to-table listname 363 | :parsefn (org-listcruncher-mk-parseitem-default 364 | :tag "\\*?row:\\*?" 365 | :bra "[" 366 | :ket "]") 367 | :order '("description" "amount" "recurrence")) 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 |
descriptionamountrecurrencenewcolend-year
item A modified by replacing values2012992020
item B102 2024
item C202 2024
item D253 2024
item X modified by operations150.04 2026
item Y entered in scientific format900.03 2025
448 | 449 | And another variant allowing to write the list with minimal markup for the tag: 450 | Here any line beginning with a bold markup string becomes a row with the description 451 | being taken as that string. I just define as tag/endtag the markup character "\*". 452 | 453 | - Defaults (color: white, form: cube, weight: 10) 454 | - **one item is heavy** (weight: 20) 455 | - **another is lighter** (weight: 5) 456 | - it has other distinguishing features (color: green, form: disk) 457 | - **item three** is the default 458 | 459 | We invoke the parsing function: 460 | 461 | (org-listcruncher-to-table listname 462 | :parsefn (org-listcruncher-mk-parseitem-default 463 | :tag "\\*" 464 | :endtag "\\*" 465 | :bra "(" 466 | :ket ")")) 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 |
descriptionweightcolorform
one item is heavy20whitecube
another is lighter5greendisk
item three10whitecube
514 | 515 | 516 | 517 | 518 | ## Configuration 519 | 520 | The way that the table structure is created from the list can be 521 | customized by providing own implementations of the parsing function 522 | and of the consolidation function that combines the parsed 523 | key/value pairs into a table. 524 | 525 | The current implementations are examples that are sufficient for 526 | the above use cases. 527 | 528 | One can easily imagine much more sophisticated parsing 529 | functions which e.g. could be applied to a **cooking recipe written 530 | with minimal concessions as to syntax**. From such a recipe one could 531 | then derive a table of ingredients, their amounts, and cooking 532 | times; all ready for being displayed as a table, to calculate the 533 | adapted amounts according to the number of expected guests, and 534 | entering the items onto your shopping list. 535 | 536 | I am planning to provide more sophisticated parsing and 537 | consolidation functions to choose from (and naturally would be 538 | happy to receive any additions from contributors). 539 | 540 | The default functions that are used can be configured using 541 | the following customization variables. 542 | 543 | - **`org-listcruncher-parse-fn`:** This variable defines the default 544 | parsing function to use if you call the org-listcruncher 545 | functions without an explicit `:parsefn` keyword agument. 546 | 547 | - **`org-listcruncher-consolidate-fn`:** This variable defines the 548 | default function for consolidating all the values that a certain 549 | key got assigned for a list item. The function must accept two 550 | arguments: KEY and LIST. KEY is the key (i.e. column value) of 551 | the row that one is interested in. LIST contains all the values 552 | for the KEY in that row, i.e. it will contain any redefinitions 553 | of the key value in subitems of this list item. The consolidation 554 | function basically defines how these values get combined into the 555 | single value that we will assign to the column in this row. The 556 | default function either replaces the previous value or allows 557 | values with operators (e.g. +=10, \*=0.5) to modify the previous 558 | value. Refer to the default function 559 | `org-listcruncher-consolidate-default` documentation. 560 | 561 | 562 | 563 | 564 | ## Using org table spreadsheet formulas to finalize the result 565 | 566 | The primary goal of `org-listcruncher-to-table` is to return a data structure 567 | (an org table structure) that can be used for further processing by code, e.g. 568 | in a babel block. 569 | 570 | But often, one will be mainly interested in a fast way to produce 571 | an org table that one immediately wants to process with the 572 | standard org table functions, e.g. just summing up some columns. 573 | Listcruncher offers a fast way for these situations: 574 | 575 | (princ (org-listcruncher-to-table listname :formula "@>$1=Total::@>$3=vsum(@I..@II)")) 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 |
descriptionnewcolamountresponsibleend-year
item A modified by replacing values29920Peter2020
item B 10Mary2024
item C 20Mary2024
item D3525Paul2024
item X modified by operations 150.0Peter2026
item Y entered in scientific format 900.0Mary2025
item Z illustrating += and -= with strings 1000Paul Mary2027
Total 2125.  
675 | 676 | Since when using **formula** the source block is not returning a Lisp 677 | table data structure, but an already rendered org table string, one 678 | needs to use `:results output`. Since we do not want the result to 679 | be put into an org example block, we also need to add the `raw` 680 | flag. In order to fill out the last row's description we just use 681 | for the initial formula the string `"@>$1=Total"`. So, the whole 682 | org block now looks like this. 683 | 684 | #+BEGIN_SRC elisp :results output raw :var listname="lstTest" :exports both 685 | (princ (org-listcruncher-to-table listname :formula "@>$1=Total::@>$3=vsum(@I..@II)")) 686 | #+END_SRC 687 | 688 | **Note:** In an earlier version of this example I used an external 689 | function `lobPostAlignTables` from [my library of babel](https://github.com/dfeich/org-babel-examples/blob/master/library-of-babel/dfeich-lob.org) to calculate and iterate the 690 | table with the formula in an org bable `:post` hook. This is no 691 | longer necessary with the addition of the formula feature. 692 | 693 | 694 | 695 | 696 | ## Changes 697 | 698 | 699 | 700 | 701 | ### version 1.0: API change 702 | 703 | I apologize for a backwards incompatible API change for 704 | `org-listcruncher-to-table listname` and 705 | `org-listcruncher-get-field listname`, which now both accept 706 | keyword parameters. This will make the functions more future proof 707 | when further function arguments need to be introduced. 708 | 709 | 710 | 711 | 712 | ### version 1.2: change for using operator values 713 | 714 | The original syntax of e.g. "-10" did not allow differentiating 715 | between subtracting 10 or setting value to "-10". Therefore the 716 | operator use is now defined using the operator followed by the 717 | equal sign: `-=`, `*=`, etc. The old syntax is still 718 | working to keep backward compatibility, but it is discouraged. 719 | 720 | 721 | 722 | 723 | ### version 1.4: introduction of the :formula feature 724 | 725 | Org table formulas can be added to the resulting table and 726 | listcruncher will invoke the org spreadsheet functions to 727 | calculate and align the table. 728 | 729 | 730 | 731 | 732 | ## Running tests 733 | 734 | If [keg](https://github.com/conao3/keg.el) is available for installing the test environment the make 735 | targets will use it to install dependencies and run the commands. 736 | 737 | Just run this inside of the git repository 738 | 739 | make test 740 | 741 | If you want to debug a single test, run 742 | 743 | TESTNAME=parseitem-default3 make debug 744 | 745 | The test (in this example `parseitem-default3`) will be run and you 746 | will be dropped into a live Emacs session. 747 | 748 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | 2 | * Org listcruncher 3 | 4 | # Note: The export of the org link with 5 | # file:https://travis....?branch=master fails to be recognized and 6 | # converted to a markdown image link. So I insert this directly. 7 | #+BEGIN_EXPORT md 8 | [![CI workflow](https://github.com/dfeich/org-listcruncher/actions/workflows/CI.yml/badge.svg)](https://github.com/dfeich/org-listcruncher/actions) 9 | [![img](https://melpa.org/packages/org-listcruncher-badge.svg)](https://melpa.org/#/org-listcruncher) 10 | #+END_EXPORT 11 | 12 | Org-listcruncher is a planning tool. Planning using lists is a very 13 | natural approach, and in terms of a data structure it is similar to 14 | a mind map. Emacs Org-mode makes it very easy and efficient to work 15 | with lists, since it offers a lot of functionality for restructuring 16 | and modifying them. 17 | 18 | Org-listcruncher provides a way to convert such Org-mode lists into 19 | a table structure following specific semantics. This tabular data 20 | structure can then be operated on by other code blocks, or it can 21 | just be exported. The list can retain all the comments, you can 22 | override values and keep the history of changes inside of it, while 23 | still being able to derive an easy data structure as an input for 24 | other planning tools further downstream. 25 | 26 | I've used it for the initial stage of planning for bigger projects. 27 | It was ideal for producing the often complex tables required by 28 | formal project proposal templates, and at some point I just exported 29 | the basic structures as a good starting point for continuing with a 30 | typical project management software. 31 | 32 | ** Installation 33 | 34 | You can get the package from [[https://melpa.org/#/org-listcruncher][MELPA]] using emacs' package manager. 35 | 36 | If you are using John Wiegley's =use-package= (which I recommend), just put the following line 37 | into your =~/emacs.d/init.el= (or =~/.emacs=) 38 | #+BEGIN_SRC elisp 39 | (use-package org-listcruncher) 40 | #+END_SRC 41 | 42 | Or more barebones, just =require= it. 43 | #+BEGIN_SRC elisp 44 | (require 'org-listcruncher) 45 | #+END_SRC 46 | 47 | ** Example usage 48 | 49 | Write a planning list and give it a name using the appropriate Org 50 | syntax (e.g. =#+NAME: lstTest=). Here is an example (look at the 51 | [[https://raw.githubusercontent.com/dfeich/org-listcruncher/master/README.org][raw form of this README.org]] to see the Org source buffer with all 52 | the markup) 53 | 54 | #+NAME: lstTest 55 | - *item:* item A modified by replacing values (amount: 15, responsible: Peter, end-year: 2020) 56 | - modification of item A (amount: 20) 57 | - another modification of item A introducing a column (newcol: 500) 58 | - modification of the modification (newcol: 299) 59 | - illustrating inheritance (responsible: Mary, end-year: 2024) 60 | - *item:* item B. Some longer explanation that may run over 61 | multiple lines (amount: 10) 62 | - *item:* item C (amount: 20) 63 | - *item:* item D (amount: 30, newcol: 35) 64 | - a modification to item C (amount: 25, responsible: Paul) 65 | - *item:* item X modified by operations (amount: 50, responsible: Peter, end-year: 2026) 66 | - modification by an operation (amount: +=50) 67 | - modification by an operation (amount: *=1.5) 68 | - *item:* item Y entered in scientific format (amount: 1e3, responsible: Mary, end-year: 2025) 69 | - modification by an operation (amount: -=1e2) 70 | - *item:* item Z illustrating += and -= with strings (amount: 1000, responsible: Peter, end-year: 2025) 71 | - adding a string (responsible: +=Paul, end-year: +=1) 72 | - adding a string (responsible: +=Mary, end-year: +=1) 73 | - removing a string (responsible: -=Peter) 74 | 75 | We can use org-listcruncher to convert this list into a table 76 | 77 | #+NAME: src-example1 78 | #+BEGIN_SRC elisp :results value :var listname="lstTest" :exports both 79 | (org-listcruncher-to-table listname) 80 | #+END_SRC 81 | 82 | #+RESULTS: src-example1 83 | | description | newcol | amount | responsible | end-year | 84 | |--------------------------------------------+--------+--------+-------------+----------| 85 | | item A modified by replacing values | 299 | 20 | Peter | 2020 | 86 | | item B | | 10 | Mary | 2024 | 87 | | item C | | 20 | Mary | 2024 | 88 | | item D | 35 | 25 | Paul | 2024 | 89 | | item X modified by operations | | 150.0 | Peter | 2026 | 90 | | item Y entered in scientific format | | 900.0 | Mary | 2025 | 91 | | item Z illustrating += and -= with strings | | 1000 | Paul Mary | 2027 | 92 | 93 | 94 | We can also provide an additional argument to affect the order of 95 | columns in which the table is produced. 96 | #+BEGIN_SRC elisp :results value :var listname="lstTest" :exports both 97 | (org-listcruncher-to-table listname :order '("description" "amount" "responsible")) 98 | #+END_SRC 99 | 100 | #+RESULTS: 101 | | description | amount | responsible | newcol | end-year | 102 | |--------------------------------------------+--------+-------------+--------+----------| 103 | | item A modified by replacing values | 20 | Peter | 299 | 2020 | 104 | | item B | 10 | Mary | | 2024 | 105 | | item C | 20 | Mary | | 2024 | 106 | | item D | 25 | Paul | 35 | 2024 | 107 | | item X modified by operations | 150.0 | Peter | | 2026 | 108 | | item Y entered in scientific format | 900.0 | Mary | | 2025 | 109 | | item Z illustrating += and -= with strings | 1000 | Paul Mary | | 2027 | 110 | 111 | 112 | 113 | It is also possible to directly obtain single table field values based on defining the 114 | row and column through the string corresponding to an item's description and its 115 | column name: 116 | 117 | #+BEGIN_SRC elisp :results value :var listname="lstTest" :exports both 118 | (org-listcruncher-get-field listname "item B" "amount") 119 | #+END_SRC 120 | 121 | #+RESULTS: 122 | : 10 123 | 124 | ** List writing rules 125 | 126 | The rules for writing such a planning list are 127 | 1. Each line contains a tag defining whether the line will become a table row. For this 128 | example I defined this as the string "item:". Rows without such a tag just serve as 129 | metadata. 130 | 2. A string following the output tag "item:" is taken as the description of the table row. 131 | 3. Each line can contain any number of key/value pairs in parentheses in the form 132 | =(key1: val1, key2: val2, ...)= 133 | 4. Lines of lower hierarchical order in the list inherit their default settings for key/values 134 | from the upper items. 135 | 5. The key value of a higher order item can be overwritten by a new new value for the same key 136 | in a lower order line. 137 | 6. If a given value is of the form +=10, -=10, /=10, *=10, i.e. an operator followed by a number, 138 | the operation is carried out on the previous value of the respective key. 139 | (Note: this changed in version 1.2, since the original use of "-10" did not 140 | allow differentiating between subtracting 10 or setting value to "-10". The 141 | old syntax is still allowed for all operators except "-") 142 | 7. If a given value is of the form +=word then "word" is 143 | added to the previous string value for this key, using space as a separator. If 144 | -=word is used, then "word" is removed from the previous string 145 | value. This allows building lists of words. 146 | 147 | 148 | ** Using alternate parsing functions 149 | You can define arbitrary parsing functions for the list items. They must 150 | obey the following API: 151 | 152 | The function receives a list item (a string) as its single 153 | argument. It must return a list (=OUTP, DESCR, VARLST=), where 154 | - =OUTP= is a boolean indicating whether this list item will become a table 155 | row 156 | - =DESCR= is the description string appearing in the table's "description" column 157 | (so this is only relevant for OUTP=True lines) 158 | - =VARLST= is the list of key/value pairs corresponding to the column name / 159 | values. 160 | 161 | Simple example functions for this purpose can be generated using 162 | the =org-listcruncher-mk-parseitem-default= generator function. It 163 | allows modifying the tag that decides whether a list item will 164 | become a table row. It also permits changing the description's 165 | terminating tag and the brackets for the key/value pairs. E.g. if I 166 | would like to match for "row:" instead for "item:", and if I would 167 | like to use square brackets, I can obtain such a function by 168 | executing. 169 | 170 | #+BEGIN_SRC elisp :exports source 171 | (org-listcruncher-mk-parseitem-default :tag"\\*?row:\\*?" :bra "[" :ket "]") 172 | #+END_SRC 173 | 174 | Let's test it using this modified list: 175 | 176 | #+NAME: lstTest2 177 | - *row:* item A modified by replacing values [amount: 15, recurrence: 1, end-year: 2020]. 178 | - modification of item A [amount: 20] 179 | - another modification of item A [newcol: 500] 180 | - modification of the modification [newcol: 299] 181 | - illustrating inheritance [recurrence: 2, end-year: 2024] 182 | - *row:* item B. Some longer explanation that may run over 183 | multiple lines [amount: 10] 184 | - *row:* item C [amount: 20] 185 | - *row:* item D [amount: 30] 186 | - a modification to item D [amount: 25, recurrence: 3] 187 | - *row:* item X modified by operations [amount: 50, recurrence: 4, end-year: 2026] 188 | - modification by an operation [amount: +50] 189 | - modification by an operation [amount: *1.5] 190 | - *row:* item Y entered in scientific format [amount: 1e3, recurrence: 3, end-year: 2025] 191 | - modification by an operation [amount: -=1e2] 192 | 193 | We invoke org-listcruncher with the above parsing function: 194 | 195 | #+NAME: src-example2 196 | #+BEGIN_SRC elisp :results value :var listname="lstTest2" :exports both 197 | (org-listcruncher-to-table listname 198 | :parsefn (org-listcruncher-mk-parseitem-default 199 | :tag "\\*?row:\\*?" 200 | :bra "[" 201 | :ket "]") 202 | :order '("description" "amount" "recurrence")) 203 | #+END_SRC 204 | 205 | #+RESULTS: src-example2 206 | | description | amount | recurrence | newcol | end-year | 207 | |-------------------------------------+--------+------------+--------+----------| 208 | | item A modified by replacing values | 20 | 1 | 299 | 2020 | 209 | | item B | 10 | 2 | | 2024 | 210 | | item C | 20 | 2 | | 2024 | 211 | | item D | 25 | 3 | | 2024 | 212 | | item X modified by operations | 150.0 | 4 | | 2026 | 213 | | item Y entered in scientific format | 900.0 | 3 | | 2025 | 214 | 215 | 216 | And another variant allowing to write the list with minimal markup for the tag: 217 | Here any line beginning with a bold markup string becomes a row with the description 218 | being taken as that string. I just define as tag/endtag the markup character "*". 219 | 220 | #+NAME: lstBoldItems 221 | - Defaults (color: white, form: cube, weight: 10) 222 | - *one item is heavy* (weight: 20) 223 | - *another is lighter* (weight: 5) 224 | - it has other distinguishing features (color: green, form: disk) 225 | - *item three* is the default 226 | 227 | We invoke the parsing function: 228 | 229 | #+BEGIN_SRC elisp :results value :var listname="lstBoldItems" :exports both 230 | (org-listcruncher-to-table listname 231 | :parsefn (org-listcruncher-mk-parseitem-default 232 | :tag "\\*" 233 | :endtag "\\*" 234 | :bra "(" 235 | :ket ")")) 236 | #+END_SRC 237 | 238 | #+RESULTS: 239 | | description | weight | color | form | 240 | |--------------------+--------+-------+------| 241 | | one item is heavy | 20 | white | cube | 242 | | another is lighter | 5 | green | disk | 243 | | item three | 10 | white | cube | 244 | 245 | ** Configuration 246 | The way that the table structure is created from the list can be 247 | customized by providing own implementations of the parsing function 248 | and of the consolidation function that combines the parsed 249 | key/value pairs into a table. 250 | 251 | The current implementations are examples that are sufficient for 252 | the above use cases. 253 | 254 | One can easily imagine much more sophisticated parsing 255 | functions which e.g. could be applied to a *cooking recipe written 256 | with minimal concessions as to syntax*. From such a recipe one could 257 | then derive a table of ingredients, their amounts, and cooking 258 | times; all ready for being displayed as a table, to calculate the 259 | adapted amounts according to the number of expected guests, and 260 | entering the items onto your shopping list. 261 | 262 | I am planning to provide more sophisticated parsing and 263 | consolidation functions to choose from (and naturally would be 264 | happy to receive any additions from contributors). 265 | 266 | The default functions that are used can be configured using 267 | the following customization variables. 268 | 269 | - =org-listcruncher-parse-fn= :: This variable defines the default 270 | parsing function to use if you call the org-listcruncher 271 | functions without an explicit =:parsefn= keyword agument. 272 | 273 | - =org-listcruncher-consolidate-fn= :: This variable defines the 274 | default function for consolidating all the values that a certain 275 | key got assigned for a list item. The function must accept two 276 | arguments: KEY and LIST. KEY is the key (i.e. column value) of 277 | the row that one is interested in. LIST contains all the values 278 | for the KEY in that row, i.e. it will contain any redefinitions 279 | of the key value in subitems of this list item. The consolidation 280 | function basically defines how these values get combined into the 281 | single value that we will assign to the column in this row. The 282 | default function either replaces the previous value or allows 283 | values with operators (e.g. +=10, *=0.5) to modify the previous 284 | value. Refer to the default function 285 | =org-listcruncher-consolidate-default= documentation. 286 | ** Using org table spreadsheet formulas to finalize the result 287 | The primary goal of =org-listcruncher-to-table= is to return a data structure 288 | (an org table structure) that can be used for further processing by code, e.g. 289 | in a babel block. 290 | 291 | But often, one will be mainly interested in a fast way to produce 292 | an org table that one immediately wants to process with the 293 | standard org table functions, e.g. just summing up some columns. 294 | Listcruncher offers a fast way for these situations: 295 | 296 | #+BEGIN_SRC elisp :results output raw :var listname="lstTest" :exports both 297 | (princ (org-listcruncher-to-table listname :formula "@>$1=Total::@>$3=vsum(@I..@II)")) 298 | #+END_SRC 299 | 300 | #+RESULTS: 301 | | description | newcol | amount | responsible | end-year | 302 | |--------------------------------------------+--------+--------+-------------+----------| 303 | | item A modified by replacing values | 299 | 20 | Peter | 2020 | 304 | | item B | | 10 | Mary | 2024 | 305 | | item C | | 20 | Mary | 2024 | 306 | | item D | 35 | 25 | Paul | 2024 | 307 | | item X modified by operations | | 150.0 | Peter | 2026 | 308 | | item Y entered in scientific format | | 900.0 | Mary | 2025 | 309 | | item Z illustrating += and -= with strings | | 1000 | Paul Mary | 2027 | 310 | |--------------------------------------------+--------+--------+-------------+----------| 311 | | Total | | 2125. | | | 312 | #+TBLFM: @>$1=Total::@>$3=vsum(@I..@II) 313 | 314 | Since when using *formula* the source block is not returning a Lisp 315 | table data structure, but an already rendered org table string, one 316 | needs to use =:results output=. Since we do not want the result to 317 | be put into an org example block, we also need to add the =raw= 318 | flag. In order to fill out the last row's description we just use 319 | for the initial formula the string ="@>$1=Total"=. So, the whole 320 | org block now looks like this. 321 | 322 | #+begin_example 323 | ,#+BEGIN_SRC elisp :results output raw :var listname="lstTest" :exports both 324 | (princ (org-listcruncher-to-table listname :formula "@>$1=Total::@>$3=vsum(@I..@II)")) 325 | ,#+END_SRC 326 | #+end_example 327 | 328 | *Note:* In an earlier version of this example I used an external 329 | function =lobPostAlignTables= from [[https://github.com/dfeich/org-babel-examples/blob/master/library-of-babel/dfeich-lob.org][my library of babel]] to calculate and iterate the 330 | table with the formula in an org bable =:post= hook. This is no 331 | longer necessary with the addition of the formula feature. 332 | 333 | ** Changes 334 | *** version 1.0: API change 335 | I apologize for a backwards incompatible API change for 336 | =org-listcruncher-to-table listname= and 337 | =org-listcruncher-get-field listname=, which now both accept 338 | keyword parameters. This will make the functions more future proof 339 | when further function arguments need to be introduced. 340 | *** version 1.2: change for using operator values 341 | The original syntax of e.g. "-10" did not allow differentiating 342 | between subtracting 10 or setting value to "-10". Therefore the 343 | operator use is now defined using the operator followed by the 344 | equal sign: ~-=~, ~*=~, etc. The old syntax is still 345 | working to keep backward compatibility, but it is discouraged. 346 | *** version 1.4: introduction of the :formula feature 347 | Org table formulas can be added to the resulting table and 348 | listcruncher will invoke the org spreadsheet functions to 349 | calculate and align the table. 350 | 351 | ** Running tests 352 | 353 | If [[https://github.com/conao3/keg.el][keg]] is available for installing the test environment the make 354 | targets will use it to install dependencies and run the commands. 355 | 356 | Just run this inside of the git repository 357 | : make test 358 | 359 | If you want to debug a single test, run 360 | : TESTNAME=parseitem-default3 make debug 361 | The test (in this example =parseitem-default3=) will be run and you 362 | will be dropped into a live Emacs session. 363 | 364 | ** Tests :noexport: 365 | 366 | A look at the main heavy lifting function and its return values: 367 | #+BEGIN_SRC elisp :results output :var listname="lstTest" 368 | (pp (org-listcruncher--parselist (save-excursion 369 | (goto-char (point-min)) 370 | (unless (search-forward-regexp (concat "^ *#\\\+NAME: .*" listname) nil t) 371 | (error "No list of this name found: %s" listname)) 372 | (forward-line 1) 373 | (org-list-to-lisp)) 374 | org-listcruncher-parse-fn 375 | nil 376 | nil)) 377 | #+END_SRC 378 | 379 | #+RESULTS: 380 | #+begin_example 381 | ((("responsible" "-=Peter") 382 | ("responsible" "+=Mary") 383 | ("end-year" "+=1") 384 | ("responsible" "+=Paul") 385 | ("end-year" "+=1") 386 | ("amount" "1000") 387 | ("responsible" "Peter") 388 | ("end-year" "2025") 389 | ("amount" "-=1e2") 390 | ("amount" "1e3") 391 | ("responsible" "Mary") 392 | ("end-year" "2025") 393 | ("amount" "*=1.5") 394 | ("amount" "+=50") 395 | ("amount" "50") 396 | ("responsible" "Peter") 397 | ("end-year" "2026") 398 | ("amount" "25") 399 | ("responsible" "Paul") 400 | ("amount" "30") 401 | ("newcol" "35") 402 | ("amount" "20") 403 | ("amount" "10") 404 | ("responsible" "Mary") 405 | ("end-year" "2024") 406 | ("newcol" "299") 407 | ("newcol" "500") 408 | ("amount" "20") 409 | ("amount" "15") 410 | ("responsible" "Peter") 411 | ("end-year" "2020")) 412 | ((("description" "item A modified by replacing values") 413 | ("newcol" "299") 414 | ("newcol" "500") 415 | ("amount" "20") 416 | ("amount" "15") 417 | ("responsible" "Peter") 418 | ("end-year" "2020")) 419 | (("description" "item B") 420 | ("amount" "10") 421 | ("responsible" "Mary") 422 | ("end-year" "2024")) 423 | (("description" "item C") 424 | ("amount" "20") 425 | ("responsible" "Mary") 426 | ("end-year" "2024")) 427 | (("description" "item D") 428 | ("amount" "25") 429 | ("responsible" "Paul") 430 | ("amount" "30") 431 | ("newcol" "35") 432 | ("responsible" "Mary") 433 | ("end-year" "2024")) 434 | (("description" "item X modified by operations") 435 | ("amount" "*=1.5") 436 | ("amount" "+=50") 437 | ("amount" "50") 438 | ("responsible" "Peter") 439 | ("end-year" "2026")) 440 | (("description" "item Y entered in scientific format") 441 | ("amount" "-=1e2") 442 | ("amount" "1e3") 443 | ("responsible" "Mary") 444 | ("end-year" "2025")) 445 | (("description" "item Z illustrating += and -= with strings") 446 | ("responsible" "-=Peter") 447 | ("responsible" "+=Mary") 448 | ("end-year" "+=1") 449 | ("responsible" "+=Paul") 450 | ("end-year" "+=1") 451 | ("amount" "1000") 452 | ("responsible" "Peter") 453 | ("end-year" "2025")))) 454 | #+end_example 455 | 456 | 457 | 458 | * Tests integrating with orgbabelhelper :noexport: 459 | 460 | 461 | #+BEGIN_SRC python :results output raw drawer :var tbl=src-example1 :colnames no 462 | import orgbabelhelper as obh 463 | 464 | df = obh.orgtable_to_dataframe(tbl, index="description") 465 | print(obh.dataframe_to_orgtable(df, caption="Example 1")) 466 | #+END_SRC 467 | 468 | #+RESULTS: 469 | :results: 470 | #+CAPTION: Example 1 471 | |description|newcol|amount|responsible|end-year| 472 | |----- 473 | |item A modified by replacing values|299|20|Peter|2020| 474 | |item B||10|Mary|2024| 475 | |item C||20|Mary|2024| 476 | |item D|35|25|Paul|2024| 477 | |item X modified by operations||150.0|Peter|2026| 478 | |item Y entered in scientific format||900.0|Mary|2025| 479 | |item Z illustrating += and -= with strings||1000|Paul Mary|2027| 480 | 481 | :end: 482 | 483 | * COMMENT Org Babel settings 484 | Local variables: 485 | org-confirm-babel-evaluate: nil 486 | End: 487 | -------------------------------------------------------------------------------- /org-listcruncher.el: -------------------------------------------------------------------------------- 1 | ;;; org-listcruncher.el --- Planning tool - Parse Org mode lists into table -*- lexical-binding: t; -*- 2 | 3 | ;; Author: Derek Feichtinger 4 | ;; Keywords: convenience 5 | ;; Package-Requires: ((seq "2.3") (emacs "26.1")) 6 | ;; Homepage: https://github.com/dfeich/org-listcruncher 7 | ;; Version: 1.4 8 | 9 | ;; This program is free software; you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation; either version 3, or (at your option) 12 | ;; any later version. 13 | ;; 14 | ;; This program is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | ;; 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with GNU Emacs. If not, see . 21 | 22 | 23 | ;;; Commentary: 24 | ;; org-listcruncher is a planning tool that allows the conversion of 25 | ;; an Org mode list to an Org table (a list of lists). The table can 26 | ;; be used by other Org tables or Org code blocks for further 27 | ;; calculations. 28 | ;; 29 | ;; Example: 30 | ;; 31 | ;; #+NAME: lstTest 32 | ;; - item: item X modified by replacing values (amount: 15, recurrence: 1, end-year: 2020) 33 | ;; - modification of item X (amount: 20) 34 | ;; - another modification of item X (other: 500) 35 | ;; - modification of the modification (other: 299) 36 | ;; - illustrating inheritance (recurrence: 2, end-year: 2024) 37 | ;; - item: item A. Some longer explanation that may run over 38 | ;; multiple lines (amount: 10) 39 | ;; - item: item B (amount: 20) 40 | ;; - item: item C (amount: 30) 41 | ;; - a modification to item C (amount: 25, recurrence: 3) 42 | ;; - item: item Y modified by operations (amount: 50, recurrence: 4, end-year: 2026) 43 | ;; - modification by an operation (amount: +50) 44 | ;; - modification by an operation (amount: *1.5) 45 | ;; - item: item Z entered in scientific format (amount: 1e3, recurrence: 3, end-year: 2025) 46 | ;; - modification by an operation (amount: -1e2) 47 | ;; 48 | ;; We can use org-listcruncher to convert this list into a table 49 | ;; 50 | ;; #+NAME: src-example1 51 | ;; #+BEGIN_SRC elisp :results value :var listname="lstTest" :exports both 52 | ;; (org-listcruncher-to-table listname) 53 | ;; #+END_SRC 54 | ;; 55 | ;; #+RESULTS: src-example1 56 | ;; | description | other | amount | recurrence | end-year | 57 | ;; |-------------------------------------+-------+--------+------------+----------| 58 | ;; | item X modified by replacing values | 299 | 20 | 1 | 2020 | 59 | ;; | item A | | 10 | 2 | 2024 | 60 | ;; | item B | | 20 | 2 | 2024 | 61 | ;; | item C | | 25 | 3 | 2024 | 62 | ;; | item Y modified by operations | | 150.0 | 4 | 2026 | 63 | ;; | item Z entered in scientific format | | 900.0 | 3 | 2025 | 64 | ;; 65 | ;; The parsing and consolidation functions for producing the table can be modified by 66 | ;; the user. Please refer to the README and to the documentation strings of the 67 | ;; functions. 68 | ;;; Code: 69 | (require 'org) 70 | (require 'cl-lib) 71 | (require 'seq) 72 | 73 | (defgroup org-listcruncher nil 74 | "Parses Org mode lists according to a parsing function and yields an org table structure." 75 | :group 'org) 76 | 77 | (defcustom org-listcruncher-parse-fn #'org-listcruncher-parseitem-default 78 | "Function used for parsing list items. 79 | 80 | The function receives a list item as its single argument. It must 81 | return a list (OUTP, DESCR, VARLST), where OUTP is a boolean 82 | indicating whether this list item will become a table row, DESCR 83 | is its description string appearing in the table, VARLST is the 84 | list of key/value pairs corresponding to the column name / 85 | values. Simple example functions for this purpose can be generated 86 | using the `org-listcruncher-mk-parseitem-default' generator 87 | function." 88 | :group 'org-listcruncher 89 | :type 'function) 90 | 91 | (defcustom org-listcruncher-consolidate-fn #'org-listcruncher-consolidate-default 92 | "Function for consolidating a sequence of values for a certain key. 93 | 94 | The function must accept two arguments: KEY and LIST. The KEY is 95 | the key selecting the (KEY VALUE) pairs from the given LIST. The 96 | function must return a single value based on consolidating the 97 | VALUEs from the given key-value pairs. Refer to the default 98 | function `org-listcruncher-consolidate-default'." 99 | :group 'org-listcruncher 100 | :type 'function ) 101 | 102 | ;; TODO: make parentheses definitions a parameter 103 | ;;;###autoload 104 | (cl-defun org-listcruncher-mk-parseitem-default (&key (tag "\\*?item:\\*?") 105 | (endtag ".") 106 | (bra "(") 107 | (ket ")")) 108 | "List item default parsing function generator for org-listcruncher. 109 | 110 | This generator can be used to produce a family of parsing 111 | functions with similar structure. It returns a parsing function 112 | that will take a list item line as its only argument. 113 | 114 | The generated parsing functions all share the following features. 115 | 1. Whether a list item will become a table row is defined by a matching 116 | TAG at the beginning of the list item. Default is \"item:\" and allowing 117 | for org bold markers. 118 | 2. The row's description is defined by the string following the TAG up to 119 | a) a character contained in the ENDTAG argument or 120 | b) the opening parenthesis BRA used for beginning the key/value pairs. 121 | The default for ENDTAG is \".\". 122 | 3. The key/value pairs are separated by commas, and a key is separated from 123 | its value by a colon key1: val1, key2: val2. The default brackets are 124 | \"(\" and \")\". 125 | 126 | The resulting function can be modified by the following keyword arguments: 127 | - TAG REGEXP defines the REGEXP used for identifying whether a line will become 128 | a table row. 129 | - ENDTAG STRING: Each character contained in that string will act as a 130 | terminator for the description of an item. 131 | - The BRA and KET keywords can be used to define strings defining the opening 132 | and closing parentheses to be used for enclosing the key/value pairs 133 | The given strings will get regexp quoted." 134 | (lambda (line) 135 | (let (outp descr varstr varlst) 136 | ;; TODO: I should make the expression for the key:val list more restrictive 137 | (if (string-match 138 | (concat 139 | "^ *\\(" tag "\\)?" ;; tag 140 | " *\\([^" endtag bra "]*\\)" ;; description terminated by endtag or bra 141 | "[^" bra "]*" ;; ignore everything until a bracket expression begins 142 | ;; key/val pairs 143 | "\\\(" (regexp-quote bra) "\\\(\\\(\\\([^:,)]+\\\):\\\([^,)]+\\\),?\\\)+\\\)" 144 | (regexp-quote ket) "\\\)?") 145 | line) 146 | (progn 147 | (setq outp (if (match-string 1 line) t nil) 148 | descr (replace-regexp-in-string " *$" "" (match-string 2 line)) 149 | varstr (match-string 4 line)))) 150 | (when varstr 151 | (setq varlst 152 | (cl-loop for elem in (split-string varstr ", *") 153 | if (string-match "\\([^:]+\\): *\\(.*\\)" elem) 154 | collect (list (match-string 1 elem) 155 | (match-string 2 elem)) into result 156 | finally return result))) 157 | (list outp descr varlst)))) 158 | 159 | 160 | (defun org-listcruncher-parseitem-default (line) 161 | "List item default parsing function for org-listcruncher. 162 | 163 | Parses the given list item LINE." 164 | (funcall (org-listcruncher-mk-parseitem-default) line)) 165 | 166 | (defun org-listcruncher--calc-orgtable (tbl) 167 | "Aligns and calculates table functions of the given org table. 168 | 169 | The table given in TBL will be processed using the standard 170 | org mode `org-table-align' and `org-table-recalculate' functions. 171 | The resulting table is returned in a string." 172 | (with-temp-buffer 173 | (erase-buffer) 174 | (insert tbl) 175 | (goto-char (point-min)) 176 | (org-mode) 177 | (while 178 | (search-forward-regexp org-table-any-line-regexp nil t) 179 | (org-table-align) 180 | (org-table-recalculate 'iterate) 181 | (goto-char (org-table-end))) 182 | (buffer-string))) 183 | 184 | (defun org-listcruncher--sparse-to-table (sparselst &optional order) 185 | "Return list of all unique keys of the list of alists in SPARSELST. 186 | 187 | If a list is provided in the ORDER argument, the table columns 188 | will be ordered according to this list. The list may contain only 189 | a subset of the items. The remaining columns will be added in the 190 | original order." 191 | (let* ((keylst 192 | ;; list of all unique keys of the list of alists in SPARSELST 193 | (cl-loop for alst in sparselst 194 | with reslst = nil 195 | collect (mapcar (lambda (kvpair) (car kvpair)) alst) into reslst 196 | finally return (seq-uniq (apply #'append reslst)))) 197 | (orderedlst (append order 198 | (cl-loop for elm in order 199 | do (setq keylst (delete elm keylst)) 200 | finally return keylst))) 201 | ;; for each key, find var values in each given row in sparselist 202 | (rows 203 | (cl-loop for alst in sparselst 204 | with reslst = nil 205 | collect (mapcar (lambda (key) 206 | (apply org-listcruncher-consolidate-fn 207 | (list key alst))) 208 | orderedlst) into reslst 209 | finally return reslst))) 210 | (if rows 211 | (append `(,orderedlst) '(hline) rows) 212 | nil))) 213 | 214 | 215 | ;;;###autoload 216 | (cl-defun org-listcruncher-to-table (listname 217 | &key (parsefn org-listcruncher-parse-fn) 218 | (order nil) 219 | (formula nil)) 220 | "Return a table structure based on parsing the Org list with name LISTNAME. 221 | 222 | Optional keyword arguments: The user may use the PARSEFN 223 | FUNCTION argument to define another parsing function for the list 224 | items. The ORDER keyword takes a list containing column names as 225 | its argument for defining the output table's desired columns 226 | order. The list may contain only a subset of the items. The 227 | remaining columns will be added in the original order. 228 | 229 | If FORMULA is non-nil the given Calc formula will be calculated 230 | by org spreadsheet functions (what usually would follow the 231 | #+TBLFM: in an org spreadsheet table). The result is no longer a 232 | Lisp table structure but a string containing the fully formatted 233 | table." 234 | (let* ((lst 235 | (save-excursion 236 | (goto-char (point-min)) 237 | (unless (search-forward-regexp (concat "^[ \t]*#\\\+NAME: .*" listname) nil t) 238 | (error "No list of this name found: %s" listname)) 239 | (forward-line 1) 240 | (org-list-to-lisp))) 241 | (tbl 242 | (org-listcruncher--sparse-to-table 243 | (cadr (org-listcruncher--parselist lst parsefn nil nil)) 244 | order))) 245 | (if formula 246 | (org-listcruncher--calc-orgtable 247 | (orgtbl-to-orgtbl (append tbl '(hline (""))) 248 | `(:tend ,(concat "#+TBLFM: " formula)))) 249 | tbl))) 250 | 251 | (defun org-listcruncher--parselist (lst parsefn inheritvars resultlst) 252 | "Parse an org list into a table structure. 253 | 254 | LST is a list as produced from `org-list-to-lisp'. PARSEFN is the 255 | parsing function for the list items. INHERITVARS is an 256 | association list of (varname value) pairs that constitute the 257 | inherited variable values from the parent. RESULTLST contains the 258 | current result structure in form of a list of association 259 | lists. Each contained association list corresponds to a later 260 | table row." 261 | (let ((ltype (car lst)) 262 | (itemstructs (cdr lst)) 263 | retvarlst) 264 | (setq retvarlst 265 | (cl-loop for struct in itemstructs 266 | with joinedsubvarlst = nil 267 | do (let ((itemtext (car struct)) 268 | (sublist (cadr struct)) 269 | itemvarlst subtreevarlst outvarlst) 270 | ;; parse this item 271 | (let* ((prsitem (apply parsefn `(,itemtext))) 272 | (outp (car prsitem)) 273 | (descr (nth 1 prsitem)) 274 | (itemvarlst (nth 2 prsitem))) 275 | ;; (princ (format "DEBUG: item [%s] varlst: %s\n" descr itemvarlst)) 276 | ;; if item has a sublist, recurse with this sublist and get varlst of this tree 277 | (when sublist 278 | (let ((parseresult (org-listcruncher--parselist sublist 279 | parsefn 280 | (append itemvarlst inheritvars) 281 | resultlst))) 282 | (setq subtreevarlst (car parseresult)) 283 | (setq resultlst (cadr parseresult))) 284 | ;;(princ (format "DEBUG: received subtreevarlst %s\n" subtreevarlst)) 285 | ) 286 | ;; only prepare an output line if this item is flagged as an output item 287 | (when outp 288 | ;; the current item's description always is placed first in the list 289 | (setq outvarlst (append `(("description" ,descr)) subtreevarlst itemvarlst inheritvars)) 290 | (setq resultlst (append resultlst (list outvarlst)))) 291 | ;; accumulate all item's varlists for returning to parent item 292 | (setq joinedsubvarlst (append subtreevarlst itemvarlst joinedsubvarlst)))) 293 | ;; we return the consolidated varlst of this tree 294 | finally return joinedsubvarlst)) 295 | (list retvarlst resultlst))) 296 | 297 | (defun org-listcruncher-consolidate-default (key lst) 298 | "Return consolidated value for KEY out of the list LST of key-value pairs. 299 | 300 | The list is given in reverse order (stack), i.e. the newest item 301 | is at the beginning. 302 | 303 | Example list:\n '((\"key\" \"+=10\") (\"key\" \"50\") (\"otherkey\" 304 | \"hello\")) 305 | 306 | When calling the function on this list with the KEY 307 | argument set to \"key\" it will return 60." 308 | (let* ((values (cl-loop for kv in lst 309 | if (equal key (car kv)) 310 | collect (cadr kv) into reslst 311 | finally return (nreverse reslst))) 312 | (result (pop values))) 313 | (cl-loop for v in values 314 | if (string-match "^\\\([+/*]=?\\\|-=\\\)\\\([0-9.]+\\\(e[0-9+]\\\)?\\\)" v) 315 | do (progn 316 | (when (eq (type-of result) 'string) 317 | (setq result (string-to-number result))) 318 | (setq result (apply (pcase (match-string 1 v) 319 | ((or "+=" "+") '+) 320 | ("-=" '-) 321 | ((or "/=" "/") '/) 322 | ((or "*=" "*") '*)) 323 | (list result 324 | (string-to-number (match-string 2 v)))))) 325 | else if (string-match "^\\(+\\|-\\)=\\(.*\\)" v) 326 | do (progn (when (or (integerp result) (floatp result)) 327 | (setq result (number-to-string result))) 328 | (pcase (match-string 1 v) 329 | ("+" (setq result (concat result " " (match-string 2 v)))) 330 | ("-" (setq result 331 | (mapconcat 332 | 'identity 333 | (remove (match-string 2 v) (split-string result " ")) 334 | " "))))) 335 | else 336 | do (setq result v)) 337 | (or result ""))) 338 | 339 | 340 | ;;;###autoload 341 | (cl-defun org-listcruncher-get-field (listname row col &key (parsefn org-listcruncher-parse-fn)) 342 | "Return field defined by ROW,COL from the table derived from LISTNAME. 343 | 344 | The given list with LISTNAME is parsed by listcruncher to obtain a table. 345 | The field is defined by the two strings for ROW and COL, where the ROW string 346 | corresponds to the contents of the item's \"description\" column and the COL 347 | string corresponds to the column's name." 348 | (let* ((tbl (org-listcruncher-to-table listname :parsefn parsefn)) 349 | (colnames (car tbl)) 350 | (colidx (cl-position col colnames :test #'equal))) 351 | (nth colidx (assoc row tbl)))) 352 | 353 | (provide 'org-listcruncher) 354 | ;;; org-listcruncher.el ends here 355 | -------------------------------------------------------------------------------- /test/test-org-listcruncher.el: -------------------------------------------------------------------------------- 1 | (require 'ert) 2 | (require 'org-listcruncher) 3 | (require 'seq) 4 | 5 | (defvar test-order '(member 6 | parseitem-default1 7 | parseitem-default2 8 | parseitem-default3 9 | parseitem-default4 10 | mk-parseitem-default1 11 | mk-parseitem-default2 12 | mk-parseitem-default3 13 | sparse-to-table1 14 | sparse-to-table2 15 | consolidate-vals1 16 | consolidate-vals2 17 | consolidate-vals3 18 | integr-get-field1 19 | integr-list-to-table1 20 | integr-calc-table-formula)) 21 | 22 | 23 | (defvar testfile "./test-org-listcruncher.org") 24 | (defvar testlist1 "*Test 25 | 26 | #+NAME: lstTest 27 | - item: item X modified by replacing values (amount: 15, recurrence: 1, end-year: 2020) 28 | - modification of item X (amount: 20) 29 | - another modification of item X (other: 500) 30 | - modification of the modification (other: 299) 31 | - illustrating inheritance (recurrence: 2, end-year: 2024) 32 | - item: item A. Some longer explanation that may run over 33 | multiple lines. Let's add another line for 34 | good measure (amount: 10) 35 | - item: item B (amount: 20) 36 | - item: item C (amount: 30) 37 | - a modification to item C (amount: 25, recurrence: 3) 38 | - item: item D (amount: 20) 39 | - replace by a negative value (amount: -10) 40 | - item: item Y modified by operations (amount: 50, recurrence: 4, end-year: 2026) 41 | - modification by an operation (amount: +=50) 42 | - modification by an operation (amount: *=1.5) 43 | - item: item Z entered in scientific format (amount: 1e3, recurrence: 3, end-year: 2025) 44 | - modification by an operation (amount: -=1e2) 45 | 46 | ") 47 | 48 | 49 | (ert-deftest parseitem-default1 () 50 | (should (equal 51 | (org-listcruncher-parseitem-default 52 | "item: First item (amount: 15, recurrence: 1, end-year: 2020)") 53 | '(t "First item" (("amount" "15") ("recurrence" "1") ("end-year" "2020")))))) 54 | 55 | (ert-deftest parseitem-default2 () 56 | (should (equal 57 | (org-listcruncher-parseitem-default 58 | "*item:* First item (amount: 15, recurrence: 1, end-year: 2020)") 59 | '(t "First item" (("amount" "15") ("recurrence" "1") ("end-year" "2020")))))) 60 | 61 | (ert-deftest parseitem-default3 () 62 | (let ((res (org-listcruncher-parseitem-default 63 | "First item (amount: 15, recurrence: 1, end-year: 2020)"))) 64 | (should (equal 65 | (nth 2 res) 66 | '(("amount" "15") ("recurrence" "1") ("end-year" "2020")))) 67 | (should (eq (car res) nil)))) 68 | 69 | ;; test for more restrictive parsing of the key/val pairs syntax 70 | (ert-deftest parseitem-default4 () 71 | (should (equal 72 | (org-listcruncher-parseitem-default 73 | "*item:* First item (amount 15, recurrence 1, end-year 2020)") 74 | '(t "First item" nil)))) 75 | 76 | (ert-deftest mk-parseitem-default1 () 77 | (should (equal 78 | (funcall (org-listcruncher-mk-parseitem-default :tag "row:") 79 | "row: First item (amount: 15, recurrence: 1, end-year: 2020)") 80 | '(t "First item" (("amount" "15") ("recurrence" "1") ("end-year" "2020")))))) 81 | 82 | (ert-deftest mk-parseitem-default2 () 83 | (should (equal 84 | (funcall (org-listcruncher-mk-parseitem-default :tag "row:" 85 | :bra "<<" 86 | :ket ">>") 87 | "row: First item <>") 88 | '(t "First item" (("amount" "15") ("recurrence" "1") ("end-year" "2020")))))) 89 | 90 | (ert-deftest mk-parseitem-default3 () 91 | (should (equal 92 | (funcall (org-listcruncher-mk-parseitem-default :tag "\\*" 93 | :endtag "\\*" 94 | :bra "(" 95 | :ket ")") 96 | "*one item is heavy* and colored (weight: 20, color: green)") 97 | '(t "one item is heavy" (("weight" "20") ("color" "green")))))) 98 | 99 | 100 | (ert-deftest sparse-to-table1 () 101 | (should (equal (org-listcruncher--sparse-to-table '((("a" 1) ("b" 2)) 102 | (("c" 3) ("a" -1)))) 103 | '(("a" "b" "c") 104 | hline 105 | (1 2 "") 106 | (-1 "" 3))))) 107 | 108 | (ert-deftest sparse-to-table2 () 109 | (should (equal (org-listcruncher--sparse-to-table '((("a" 1) ("b" 2)) 110 | (("c" 3) ("a" -1))) 111 | '("b" "c")) 112 | '(("b" "c" "a") 113 | hline (2 "" 1) 114 | ("" 3 -1))))) 115 | 116 | ;; backward compatible syntax for +,/,* 117 | (ert-deftest consolidate-vals1 () 118 | (should 119 | (equal 120 | 300.0 121 | (org-listcruncher-consolidate-default "amount" '(("description" "First item ") 122 | ("amount" "*3") 123 | ("amount" "/2") 124 | ("amount" "+100") 125 | ("amount" "1e2") 126 | ("amount" "+20") 127 | ("amount" "123") 128 | ("recurrence" "1") 129 | ("end-year" "2020")))))) 130 | ;; new syntax += -= ... 131 | (ert-deftest consolidate-vals2 () 132 | (should 133 | (equal 134 | 300.0 135 | (org-listcruncher-consolidate-default "amount" '(("description" "First item ") 136 | ("amount" "*3") 137 | ("amount" "/2") 138 | ("amount" "+100") 139 | ("amount" "1e2") 140 | ("amount" "+20") 141 | ("amount" "123") 142 | ("recurrence" "1") 143 | ("end-year" "2020")))))) 144 | 145 | ;; string-list concatenation and removal 146 | (ert-deftest consolidate-vals3 () 147 | (should 148 | (equal 149 | "AAAA CCCC" 150 | (org-listcruncher-consolidate-default "key" '(("description" "Some item") 151 | ("key" "-=BBBB") 152 | ("key" "+=CCCC") 153 | ("key" "+=BBBB") 154 | ("key" "AAAA") 155 | ("otherkey" "xyz")))))) 156 | 157 | 158 | (ert-deftest integr-get-field1 () 159 | (should (equal 160 | (with-temp-buffer 161 | (insert testlist1) 162 | (org-mode) 163 | (org-listcruncher-get-field "lstTest" "item B" "amount")) 164 | "20"))) 165 | 166 | (ert-deftest integr-list-to-table1 () 167 | (should (equal 168 | (with-temp-buffer 169 | (insert testlist1) 170 | (org-mode) 171 | (org-listcruncher-to-table "lstTest")) 172 | '(("description" "other" "amount" "recurrence" "end-year") 173 | hline 174 | ("item X modified by replacing values" "299" "20" "1" "2020") 175 | ("item A" "" "10" "2" "2024") 176 | ("item B" "" "20" "2" "2024") 177 | ("item C" "" "25" "3" "2024") 178 | ("item D" "" "-10" "2" "2024") 179 | ("item Y modified by operations" "" 150.0 "4" "2026") 180 | ("item Z entered in scientific format" "" 900.0 "3" "2025"))))) 181 | 182 | (ert-deftest integr-calc-table-formula () 183 | ;; this test may be a bit too dependent on the exact formatting and whitespace 184 | ;; in the result. We leave it for now. 185 | (should (equal 186 | (with-temp-buffer 187 | (insert testlist1) 188 | (org-mode) 189 | (org-listcruncher-to-table "lstTest" :formula "@>$1=Total::@>$3=vsum(@I..@II)")) 190 | "| description | other | amount | recurrence | end-year | 191 | |-------------------------------------+-------+--------+------------+----------| 192 | | item X modified by replacing values | 299 | 20 | 1 | 2020 | 193 | | item A | | 10 | 2 | 2024 | 194 | | item B | | 20 | 2 | 2024 | 195 | | item C | | 25 | 3 | 2024 | 196 | | item D | | -10 | 2 | 2024 | 197 | | item Y modified by operations | | 150.0 | 4 | 2026 | 198 | | item Z entered in scientific format | | 900.0 | 3 | 2025 | 199 | |-------------------------------------+-------+--------+------------+----------| 200 | | Total | | 1115. | | | 201 | #+TBLFM: @>$1=Total::@>$3=vsum(@I..@II)" 202 | ))) 203 | -------------------------------------------------------------------------------- /test/test-org-listcruncher.org: -------------------------------------------------------------------------------- 1 | * Basic example 2 | 3 | #+NAME: lsttest 4 | - item: First item (kCHF: 15, recurrence: 1, end-year: 2020) 5 | - modification of the first item (kCHF: 20) 6 | - another modification of the first item (other: 500) 7 | - modification of the modification (other: 299) 8 | - item: second item (kCHF: 50, recurrence: 4, end-year: 2026) 9 | - category (recurrence: 2, end-year: 2024) 10 | - item: a category item A (kCHF: 10) 11 | - item: a category item B (kCHF: 20) 12 | - item: a category item C (kCHF: 30) 13 | - a modification to category item C (kCHF: 25, recurrence: 3) 14 | 15 | * Example with adding to a former key value 16 | 17 | This shows how the consolidation function works when encountering 18 | a value with an operator 19 | 20 | #+NAME: lsttestAdd 21 | - item: First item (kCHF: 100, recurrence: 1, end-year: 2020) 22 | - modification of the first item (kCHF: +20) 23 | - a new val for kCHF for the first item (kCHF: 150) 24 | - a new modification of the value (kCHF: +30) 25 | - item: second item (kCHF: 50, recurrence: 4, end-year: 2026) 26 | - category (recurrence: 2, end-year: 2024) 27 | - item: a category item A (kCHF: 10) 28 | - item: a category item B (kCHF: 20) 29 | - item: a category item C (kCHF: 30) 30 | - a modification to category item C (kCHF: 25, recurrence: 3) 31 | 32 | 33 | #+BEGIN_SRC elisp :results output :var lname="lsttestAdd" :exports both 34 | (pp (org-listcruncher--parselist (save-excursion 35 | (goto-char (point-min)) 36 | (unless (search-forward-regexp (concat "^ *#\\\+NAME: .*" lname) nil t) 37 | (error "No list of this name found: %s" lname)) 38 | (forward-line 1) 39 | (org-list-to-lisp)) 40 | nil 41 | nil)) 42 | #+END_SRC 43 | 44 | #+RESULTS: 45 | #+begin_example 46 | ((("kCHF" "25") 47 | ("recurrence" "3") 48 | ("kCHF" "30") 49 | ("kCHF" "20") 50 | ("kCHF" "10") 51 | ("recurrence" "2") 52 | ("end-year" "2024") 53 | ("kCHF" "50") 54 | ("recurrence" "4") 55 | ("end-year" "2026") 56 | ("kCHF" "+30") 57 | ("kCHF" "150") 58 | ("kCHF" "+20") 59 | ("kCHF" "100") 60 | ("recurrence" "1") 61 | ("end-year" "2020")) 62 | ((("description" "First item ") 63 | ("kCHF" "+30") 64 | ("kCHF" "150") 65 | ("kCHF" "+20") 66 | ("kCHF" "100") 67 | ("recurrence" "1") 68 | ("end-year" "2020")) 69 | (("description" "second item ") 70 | ("kCHF" "50") 71 | ("recurrence" "4") 72 | ("end-year" "2026")) 73 | (("description" "a category item A ") 74 | ("kCHF" "10") 75 | ("recurrence" "2") 76 | ("end-year" "2024")) 77 | (("description" "a category item B ") 78 | ("kCHF" "20") 79 | ("recurrence" "2") 80 | ("end-year" "2024")) 81 | (("description" "a category item C ") 82 | ("kCHF" "25") 83 | ("recurrence" "3") 84 | ("kCHF" "30") 85 | ("recurrence" "2") 86 | ("end-year" "2024")))) 87 | #+end_example 88 | 89 | * Issue #6 90 | ** What the bug reporter tried to do 91 | #+begin_src elisp 92 | (let ((data '((1 "Team1" (("member 1" "data1") 93 | ("member 2" "data2") 94 | ("member 3" "data3"))) 95 | (2 "Team2" (("member 1" "data1") 96 | ("member 2" "data2") 97 | ("member 3" "data3")))))) 98 | (org-listcruncher-to-table data) 99 | ) 100 | #+end_src 101 | 102 | and he gets and error: 103 | : search-forward-regexp: Wrong type argument: characterp, (1 "Team1" (("member 1" "data1") ("member 2" "data2") ("member 3" "data3"))) 104 | 105 | ** What he probably wanted to do 106 | 107 | 108 | #+NAME: issue6 109 | - item: Team1 (member_1: data1, member_2: data2, member_3: data3) 110 | - item: Team2 (member_1: data1, member_2: data2, member_3: data3) 111 | 112 | #+BEGIN_SRC elisp :results value :var listname="issue6" :exports both 113 | (org-listcruncher-to-table listname) 114 | #+END_SRC 115 | 116 | #+RESULTS: 117 | | description | member_1 | member_2 | member_3 | 118 | |-------------+----------+----------+----------| 119 | | Team1 | data1 | data2 | data3 | 120 | | Team2 | data1 | data2 | data3 | 121 | 122 | ** A look at the data structures 123 | 124 | I chose to find the list and execute =org-list-to-lisp= myself on it, because 125 | if the list is just passed by reference (like in the following block) the first 126 | level of list items is losing information 127 | 128 | #+BEGIN_SRC elisp :results value :var list=issue6 :exports both 129 | (pp list) 130 | #+END_SRC 131 | 132 | #+RESULTS: 133 | : (("item: Team1 (member_1: data1, member_2: data2, member_3: data3)") 134 | : ("item: Team2 (member_1: data1, member_2: data2, member_3: data3)")) 135 | 136 | And now I locate the list myself 137 | #+BEGIN_SRC elisp :results output :var listname="issue6" :exports both 138 | (let ((lst 139 | (save-excursion 140 | (goto-char (point-min)) 141 | (unless (search-forward-regexp (concat "^[ \t]*#\\\+NAME: .*" listname) nil t) 142 | (error "No list of this name found: %s" listname)) 143 | (forward-line 1) 144 | (org-list-to-lisp)))) 145 | (pp lst)) 146 | #+END_SRC 147 | 148 | #+RESULTS: 149 | : (unordered 150 | : ("item: Team1 (member_1: data1, member_2: data2, member_3: data3)") 151 | : ("item: Team2 (member_1: data1, member_2: data2, member_3: data3)")) 152 | 153 | 154 | 155 | #+NAME: issue6b 156 | - item: Team1 (member_1: data1, member_2: data2, member_3: data3) 157 | - I changed my mind (member_2: DATA2) 158 | - item: Team2 (member_1: data1, member_2: data2, member_3: data3) 159 | - I changed my mind again (member_3: DATA3) 160 | 161 | #+BEGIN_SRC elisp :results value :var listname="issue6b" :exports both 162 | (org-listcruncher-to-table listname) 163 | #+END_SRC 164 | 165 | #+RESULTS: 166 | | description | member_2 | member_1 | member_3 | 167 | |-------------+----------+----------+----------| 168 | | Team1 | DATA2 | data1 | data3 | 169 | | Team2 | data2 | data1 | DATA3 | 170 | 171 | 172 | #+BEGIN_SRC elisp :results value :var list=issue6b :exports both 173 | (pp list) 174 | #+END_SRC 175 | 176 | #+RESULTS: 177 | : (("item: Team1 (member_1: data1, member_2: data2, member_3: data3)" 178 | : (unordered 179 | : ("I changed my mind (member_2: DATA2)"))) 180 | : ("item: Team2 (member_1: data1, member_2: data2, member_3: data3)" 181 | : (unordered 182 | : ("I changed my mind again (member_3: DATA3)")))) 183 | 184 | #+BEGIN_SRC elisp :results output :var listname="issue6b" :exports both 185 | (let ((lst 186 | (save-excursion 187 | (goto-char (point-min)) 188 | (unless (search-forward-regexp (concat "^[ \t]*#\\\+NAME: .*" listname) nil t) 189 | (error "No list of this name found: %s" listname)) 190 | (forward-line 1) 191 | (org-list-to-lisp)))) 192 | (pp lst)) 193 | #+END_SRC 194 | 195 | #+RESULTS: 196 | : (unordered 197 | : ("item: Team1 (member_1: data1, member_2: data2, member_3: data3)" 198 | : (unordered 199 | : ("I changed my mind (member_2: DATA2)"))) 200 | : ("item: Team2 (member_1: data1, member_2: data2, member_3: data3)" 201 | : (unordered 202 | : ("I changed my mind again (member_3: DATA3)")))) 203 | 204 | * Issue #8 205 | 206 | Problem: When encountering a value change string like "-50" the 207 | current default value consolidation function always interprets 208 | that the user wants to subtract 50 from the former value. But there 209 | must also be a way to set the value directly to "-50". Example: 210 | 211 | #+NAME: lst_issue8 212 | - item: row1 (key1: -20) 213 | - I wnat to change the key1 value to -50, so (key1: -50) 214 | 215 | #+BEGIN_SRC elisp :results value :var listname="lst_issue8" :exports both 216 | (org-listcruncher-to-table listname) 217 | #+END_SRC 218 | 219 | #+RESULTS: 220 | | description | key1 | 221 | |-------------+------| 222 | | row1 | -70 | 223 | 224 | 225 | Now, org-listcruncher allows anyhow that expert users can supply their own 226 | consolidation functions, but I think the default should be smarter. In order 227 | to conserve as much backwards compatibility as possible, I will implement 228 | the it will use "+=","-=" syntax, but still allow simple operators for everything 229 | except the subtraction. "-50" will always be interpreted as setting the value 230 | to -50. 231 | 232 | #+NAME: lst_issue8b 233 | - item: row1 (key1: -20) 234 | - I wnat to change the key1 value to -50, so (key1: -50) 235 | - item: row2 (key1: -20) 236 | - I want to subtract -50, so (key1: -=50) 237 | - item: row3 (key1: -20). more complex example 238 | - (key1: +=30) should be 10 239 | - (key1: *2) should be 20 240 | - (key1: *=3) should be 60 241 | - (key1: /=12) should be 5 242 | 243 | 244 | #+BEGIN_SRC elisp :results value :var listname="lst_issue8b" :exports both 245 | (org-listcruncher-to-table listname) 246 | #+END_SRC 247 | 248 | #+RESULTS: 249 | | description | key1 | 250 | |-------------+------| 251 | | row1 | -50 | 252 | | row2 | -70 | 253 | | row3 | 5 | 254 | 255 | * provide string concatenating feature 256 | 257 | 258 | #+NAME: lst_StringAdd 259 | - item: row1 (key1: AAAA) 260 | - I want to change the key1 value to BBBB, so (key1: BBBB) 261 | - item: row2 (key1: AAAA) 262 | - I want to add a string, so (key1: +=BBBB) 263 | - item: row3 (key1: AAAA) 264 | - I want to add a string, so (key1: +=BBBB) 265 | - I want to add a string, so (key1: +=CCCC) 266 | - I want to remove a string, so (key1: -=BBBB) 267 | 268 | #+BEGIN_SRC elisp :results value :var listname="lst_StringAdd" :exports both 269 | (org-listcruncher-to-table listname) 270 | #+END_SRC 271 | 272 | #+RESULTS: 273 | | description | key1 | 274 | |-------------+-----------| 275 | | row1 | BBBB | 276 | | row2 | AAAA BBBB | 277 | | row3 | AAAA CCCC | 278 | --------------------------------------------------------------------------------