diff --git a/README.markdown b/README.markdown index c15f955..2bae991 100644 --- a/README.markdown +++ b/README.markdown @@ -257,6 +257,8 @@ Possible values are : ### Dates + `d` : Insert the current date ++ `p` : Postpone the due date (accepts a count) ++ `P` : Decrement the due date (accepts a count) + `date` : (Insert mode) Insert the current date + `due:` : (Insert mode) Insert `due:` followed by the current date + `DUE:` : (Insert mode) Insert `DUE:` followed by the current date diff --git a/autoload/todo.vim b/autoload/todo.vim index e694c17..0bfe65e 100644 --- a/autoload/todo.vim +++ b/autoload/todo.vim @@ -359,6 +359,272 @@ function! Todo_txt_InsertSpaceIfNeeded(str) retur a:str endfunction +" function todo#ChangeDueDate {{{2 +function! todo#ChangeDueDate(units, unit_type) + " Change the due:date on the current line by a number of days, months or + " years + " + " units is the number of unit_type to add or subtract, integer values only + " unit_type may be one of 'd' (days), 'm' (months) or 'y' (years), as + " handled by todo#DateAdd + + let l:currentline = getline('.') + + " Don't operate on complete tasks + if l:currentline =~# '^x ' + return + endif + + let l:dueDateRex = '\v\c(^|\s)due:\zs\d{4}\-\d{2}\-\d{2}\ze(\s|$)' + + let l:duedate = matchstr(l:currentline, l:dueDateRex) + if l:duedate == '' + " No due date on current line, then add the due date as an offset from + " current date. I.e. a v:count of 1 is due tomorrow, etc + if l:currentline =~? '\v\c(^|\s)due:' + " Has an invalid due: keyword, so don't add another, and don't + " change the line + return + endif + let l:duedate = strftime('%Y-%m-%d') + let l:currentline .= ' due:' . l:duedate + endif + + let l:duedate = todo#DateStringAdd(l:duedate, v:count1 * a:units, a:unit_type) + + if setline('.', substitute(l:currentline, l:dueDateRex, l:duedate, '')) != 0 + throw "Failed to set line" + endif +endfunction "}}} + +" General date calculation functions {{{1 + +" function todo#GetDaysInMonth {{{2 +function! todo#GetDaysInMonth(month, year) + " Given a month and year, returns the number of days in the month, taking + " leap years into consideration. + + if index([1, 3, 5, 7, 8, 10, 12], a:month) >= 0 + return 31 + elseif index([4, 6, 9, 11], a:month) >= 0 + return 30 + else + " February, leap year fun. + if a:year % 4 != 0 + return 28 + elseif a:year % 100 != 0 + return 29 + elseif a:year % 400 != 0 + return 28 + else + return 29 + endif + endif +endfunction + +" function todo#DateAdd {{{2 +function! todo#DateAdd(year, month, day, units, unit_type) + " Add or subtract days, months or years from a date + " + " Date must be passed in components of year, month and day, all integers + " units is the number of unit_type to add or subtract, integer values only + " unit_type may be one of: + " d days + " m months, keeps the day of the month static except in the case + " that the day is the last day in the month or the day is higher + " than the number of days in the resultant month, where the result + " will stick to the end of the month. Examples: + " 2017-01-15 +1m 2017-02-15 +1m 2017-03-15 +1m 2017-04-15 + " 2017-01-31 +1m 2017-02-28 +1m 2017-03-31 +1m 2017-04-30 + " 2017-01-30 +1m 2017-02-28 +1m 2017-03-31 + " 2017-01-30 +2m 2017-03-30 + " y years + + + " It is my understanding that VIM does not have date math functionality + " built in. Given we only have to deal with dates, and not times, it isn't + " all that scary to roll our own - we just need to watch out for leap years. + + " Check and clean up input + if index(["d", "m", "y"], a:unit_type) < 0 + throw 'Invalid unit "'. a:unit_type . '" passed to todo#DateAdd()' + endif + + let l:d = str2nr(a:day) + let l:m = str2nr(a:month) + let l:y = str2nr(a:year) + let l:i = str2nr(a:units) + + " Years can be handled simply as 12 x months + if a:unit_type == "y" + let l:utype = "m" + let l:i = l:i * 12 + else + let l:utype = a:unit_type + endif + + " Check and clean up input + if l:m < 1 + if l:m == 0 + let l:m = str2nr(strftime('%m')) + else + let l:m = 1 + endif + endif + if l:m > 12 + if l:i < 0 && l:utype == "m" + " Subtracting an invalid (high) month + " See comments for passing a high day below. Same reason for this. + let l:m = 13 + else + let l:m = 12 + endif + endif + if l:y < 1900 " See end of function for rationale + if l:y == 0 + let l:y = str2nr(strftime('%Y')) + else + let l:y = 1900 + endif + endif + + " Grab number of days in the month specified + let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y) + + " Check and clean up input + if l:d < 1 + if l:d == 0 + let l:d = str2nr(strftime('%d')) + else + let l:d = 1 + endif + endif + " Allow passing a high day, this allows subtraction to be more sane when + " the day is out of bounds, i.e. 2017-04-80 should probably come out as + " 2017-04-30 not 2017-04-29. Addition deals with days being out of + " bounds (high) fine, and if days are untouched, out of bounds user + " input is caught at the end of the function. + " if l:d > l:daysInMonth + " let l:d = l:daysInMonth + " endif + + if l:utype == "d" + " Adding DAYS + while l:i > 0 + let l:d += 1 + if l:d > l:daysInMonth + let l:d = 1 + let l:m += 1 + if l:m > 12 + let l:m = 1 + let l:y += 1 + endif + let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y) + endif + let l:i -= 1 + endwhile + " Subtracting DAYS + while l:i < 0 + let l:d -= 1 + if l:d < 1 + let l:m -= 1 + if l:m < 1 + if l:y > 1900 + let l:m = 12 + let l:y -= 1 + else + let l:d = 1 + let l:m = 1 + break + endif + endif + let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y) + let l:d = l:daysInMonth + endif + let l:i += 1 + endwhile + elseif l:utype == "m" + if l:d >= l:daysInMonth + let l:wasLastDayOfMonth = 1 + else + let l:wasLastDayOfMonth = 0 + endif + " Adding MONTHS + while l:i > 0 + let l:m += 1 + if l:m > 12 + let l:m = 1 + let l:y += 1 + endif + let l:i -= 1 + endwhile + " Subtracting MONTHS + while l:i < 0 + let l:m -= 1 + if l:m < 1 + if l:y > 1900 + let l:m = 12 + let l:y -= 1 + else + let l:m = 1 + endif + endif + let l:i += 1 + endwhile + let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y) + if l:wasLastDayOfMonth + let l:d = l:daysInMonth + endif + endif + + " Enforce some limits beyond which, I don't want to support. + if l:y < 1900 + " Seeing as the date is going to be converted back to a string, dates + " less that 1000 are bound to cause bugs. Given this is an app for tasks + " you are doing in the here and now, I'm not supporting way back in the + " past. + let l:y = 1900 + let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y) + endif + " If we mess with the year (just above), or the user passes a day higher + " than the month, catch it here. + if l:d > l:daysInMonth + let l:d = l:daysInMonth + endif + return [l:y, l:m, l:d] +endfunction + +" function todo#DateStringAdd {{{2 +function! todo#DateStringAdd(date, units, unit_type) + " A very thin overload of todo#DateAdd() that takes and returns the date as + " a string rather than in [year, month, day] component form. + " + " Date must be passed in "YYYY-MM-DD" format, and is returned in this form + " also. + + let [l:year, l:month, l:day] = todo#ParseDate(a:date) + let [l:year, l:month, l:day] = todo#DateAdd(l:year, l:month, l:day, a:units, a:unit_type) + let l:resulting_date = printf('%04d', l:year) . '-' . printf('%02d', l:month) . '-' . printf('%02d', l:day) + return l:resulting_date +endfunction + +" function todo#ParseDate {{{2 +function! todo#ParseDate(datestring) + " Given a date as a string in the format "YYYY-MM-DD", split the date into a + " list [year, month, day] + " + " Does not check if the date is valid other than being digits. Will throw an + " exception if the text does not match the expected date format. + + if a:datestring !~? '\v^(\d{4})\-(\d{2})\-(\d{2})$' + throw "Invalid date passed '" . a:datestring . "'." + endif + let l:year = str2nr(strpart(a:datestring, 0, 4)) + let l:month = str2nr(strpart(a:datestring, 5, 2)) + let l:day = str2nr(strpart(a:datestring, 8, 2)) + return [l:year, l:month, l:day] +endfunction "}}} + " Completion {{{1 " Simple keyword completion on all buffers {{{2 diff --git a/doc/todo.txt b/doc/todo.txt index 91a8809..2b27039 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -278,6 +278,10 @@ Possible values are : `d` : Insert the current date + `p` : Postpone the due date (accepts a count) + + `P` : Decrement the due date (accepts a count) + `date` : (Insert mode) Insert the current date `due:` : (Insert mode) Insert `due:` followed by the current date diff --git a/ftplugin/todo.vim b/ftplugin/todo.vim index ceba715..cd726c1 100644 --- a/ftplugin/todo.vim +++ b/ftplugin/todo.vim @@ -23,7 +23,12 @@ setlocal wrapmargin=0 " Mappings {{{1 -if !exists("g:Todo_txt_do_not_map") +nnoremap TodotxtIncrementDueDateNormal :call ChangeDueDateWrapper(1, "\TodotxtIncrementDueDateNormal") +vnoremap TodotxtIncrementDueDateVisual :call ChangeDueDateWrapper(1, "\TodotxtIncrementDueDateVisual") +nnoremap TodotxtDecrementDueDateNormal :call ChangeDueDateWrapper(-1, "\TodotxtDecrementDueDateNormal") +vnoremap TodotxtDecrementDueDateVisual :call ChangeDueDateWrapper(-1, "\TodotxtDecrementDueDateVisual") + +if !exists("g:Todo_txt_do_not_map") || ! g:Todo_txt_do_not_map " Sort todo by (first) context noremap sc :call todo#HierarchicalSort('@', '', 1) @@ -74,6 +79,11 @@ if !exists("g:Todo_txt_do_not_map") " try fix format {{{2 nnoremap