1 /* 2 ------------------------------------------------------------------- 3 4 Copyright (C) 2014, Edwin van Leeuwen 5 6 This file is part of todod todo list manager. 7 8 Todod is free software; you can redistribute it and/or modify 9 it under the terms of the GNU General Public License as published by 10 the Free Software Foundation; either version 3 of the License, or 11 (at your option) any later version. 12 13 Todod is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 GNU General Public License for more details. 17 18 You should have received a copy of the GNU General Public License 19 along with Todod. If not, see <http://www.gnu.org/licenses/>. 20 21 ------------------------------------------------------------------- 22 */ 23 24 module todod.todo; 25 26 import std.string; 27 import std.regex; 28 29 import std.json; 30 31 import std.stdio; 32 import std.file; 33 import std.conv; 34 35 import std.algorithm; 36 import std.range; 37 import std.array; 38 39 import std.random; 40 41 import std.uuid; 42 43 import todod.date; 44 import todod.dependency; 45 import todod.random; 46 import todod.set; 47 import todod.shell; 48 import todod.storage; 49 import todod.tag; 50 51 /// A Todo 52 class Todo { 53 UUID id; 54 55 Date[] progress; /// Keep track of how long/often we've worked on this 56 57 Tags tags; 58 59 Date creation_date; 60 Date due_date; 61 62 import std.datetime : SysTime; 63 SysTime done_time; 64 65 double weight = 1; /// Weight/priority of this Todo 66 67 private this() { 68 done_time = SysTime(0); 69 } 70 71 this( string tle ) { 72 auto date_tup = parseAndRemoveDueDate( tle ); 73 due_date = date_tup[0]; 74 75 creation_date = Date.now; 76 mytitle = date_tup[1]; 77 78 id = randomUUID; 79 80 done_time = SysTime(0); 81 } 82 83 @property string title() const { 84 return mytitle; 85 } 86 87 override bool opEquals(Object t) const { 88 auto td = cast(Todo)(t); 89 return mytitle == td.mytitle; 90 } 91 92 override int opCmp(Object t) const { 93 auto td = cast(Todo)(t); 94 if ( this == td ) 95 return 0; 96 else if ( title < td.title ) 97 return -1; 98 return 1; 99 } 100 101 private: 102 string mytitle; 103 } 104 105 unittest { 106 Todo t1 = new Todo( "Todo 1" ); 107 assert( t1.title == "Todo 1" ); 108 assert( !t1.id.empty ); 109 110 assert( t1.weight == 1 ); 111 } 112 113 string toString( Todo t ) { 114 string str = t.title ~ " [ "; 115 foreach ( tag; t.tags ) 116 str ~= tag.name ~ ", "; 117 str ~= "]"; 118 return str; 119 } 120 121 JSONValue toJSON( Todo t ) { 122 JSONValue[string] jsonTODO; 123 jsonTODO["title"] = t.title; 124 JSONValue[] tags; 125 foreach( tag; t.tags ) 126 tags ~= tag.toJSON(); 127 jsonTODO["tags"] = JSONValue(tags); 128 129 string[] progress_array; 130 foreach( d; t.progress ) 131 progress_array ~= d.toStringDate; 132 jsonTODO["progress"] = progress_array; 133 jsonTODO["creation_date"] = t.creation_date.toStringDate; 134 jsonTODO["due_date"] = t.due_date.toStringDate; 135 jsonTODO["id"] = t.id.toString; 136 jsonTODO["weight"] = t.weight; 137 jsonTODO["done_time"] = t.done_time.toISOExtString; 138 return JSONValue( jsonTODO ); 139 } 140 141 Todo toTodo( const JSONValue json ) { 142 Todo t = new Todo(); 143 const JSONValue[string] jsonAA = json.object; 144 t.mytitle = jsonAA["title"].str; 145 foreach ( tag; jsonAA["tags"].array ) 146 t.tags.add( Tag.parseJSON( tag ) ); 147 foreach ( js; jsonAA["progress"].array ) 148 t.progress ~= Date( js.str ); 149 t.creation_date = Date( jsonAA["creation_date"].str ); 150 if ("due_date" in jsonAA) 151 t.due_date = Date( jsonAA["due_date"].str ); 152 if ("id" in jsonAA) 153 t.id = UUID( jsonAA["id"].str ); 154 if ("weight" in jsonAA) { 155 if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 156 t.weight = jsonAA["weight"].floating; 157 else 158 t.weight = cast(double)(jsonAA["weight"].integer); 159 } 160 161 if ("done_time" in jsonAA) { 162 import std.datetime : SysTime; 163 t.done_time = SysTime.fromISOExtString(jsonAA["done_time"].str); 164 } 165 return t; 166 } 167 168 Todo toTodo( const JSONValue json, Tags tags ) { 169 Todo t = new Todo(); 170 const JSONValue[string] jsonAA = json.object; 171 t.mytitle = jsonAA["title"].str; 172 foreach ( tag; jsonAA["tags"].array ) { 173 auto newTag = Tag.parseJSON( tag ); 174 auto found = tags.find( newTag ); 175 if (found.length>0) 176 t.tags.add( found[0] ); 177 } 178 foreach ( js; jsonAA["progress"].array ) 179 t.progress ~= Date( js.str ); 180 t.creation_date = Date( jsonAA["creation_date"].str ); 181 if ("due_date" in jsonAA) 182 t.due_date = Date( jsonAA["due_date"].str ); 183 if ("id" in jsonAA) 184 t.id = UUID( jsonAA["id"].str ); 185 if ("weight" in jsonAA) { 186 if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 187 t.weight = jsonAA["weight"].floating; 188 else 189 t.weight = cast(double)(jsonAA["weight"].integer); 190 } 191 192 if ("done_time" in jsonAA) { 193 import std.datetime : SysTime; 194 t.done_time = SysTime.fromISOExtString(jsonAA["done_time"].str); 195 } 196 return t; 197 } 198 199 unittest { 200 Todo t1 = new Todo( "Todo 1 +tag" ); 201 assert( toJSON( t1 ).toTodo == t1 ); 202 203 string missingDate = "{\"title\":\"Todo 1\",\"tags\":[{\"name\":\"tag\",\"id\":\"00000000-0000-0000-0000-000000000000\"}],\"progress\":[],\"creation_date\":\"2014-05-06\"}"; 204 205 assert( toTodo( parseJSON(missingDate) ).title == "Todo 1" ); 206 } 207 208 /// Days since last progress. If no progress has been made then days since creation 209 auto lastProgress( const Todo t ) { 210 Date currentDate = Date.now; 211 if ( t.progress.length > 0 ) 212 return currentDate.substract( t.progress[$-1] ); 213 else 214 return currentDate.substract( t.creation_date ); 215 } 216 217 auto markDone(ref Todo t, Tag doneTag) { 218 import std.datetime : Clock; 219 t.tags.add(doneTag); 220 t.done_time = Clock.currTime(); 221 return t; 222 } 223 224 unittest { 225 Todo t = new Todo("test"); 226 assert(t.tags.filter!((a) => a.name == "done").empty); 227 t.markDone(new Tag("done")); 228 assert(!t.tags.filter!((a) => a.name == "done").empty); 229 } 230 231 /** 232 Working on list of todos 233 */ 234 alias Todos = Set!Todo; 235 version(unittest) { 236 Todos generateSomeTodos() { 237 Todo t1 = new Todo( "Todo 1" ); 238 t1.tags.add( [ new Tag( "tag1" ),new Tag( "tag2" ), new Tag( "tag3" ) ] ); 239 Todo t2 = new Todo( "Bla" ); 240 t2.tags.add( [ new Tag( "tag2" ), new Tag( "tag4" ) ] ); 241 Todos mytodos; 242 mytodos.add( [t2,t1] ); 243 return mytodos; 244 } 245 } 246 247 unittest { 248 auto ts = generateSomeTodos; 249 assert( ts[1].tags.length == 3 ); 250 ts[1].tags.add( new Tag( "tag5" ) ); 251 assert( ts[1].tags.length == 4 ); 252 } 253 254 /** 255 Select a weighted random set of Todos 256 */ 257 Todo[] random(TODOS)(TODOS ts, Tags allTags, TagDelta selected, 258 string searchString, 259 in Dependencies deps, 260 in double[string] defaultWeights, size_t no = 5 ) { 261 if (ts.length > no) { 262 return randomGillespie( ts, allTags, selected, searchString, deps, 263 defaultWeights, no ); 264 } 265 return ts.array; 266 } 267 268 unittest { 269 auto mytodos = generateSomeTodos().array; 270 assert( mytodos[0].title == "Bla" ); 271 assert( mytodos[1].title == "Todo 1" ); 272 } 273 274 /// Return all existing tags 275 Tags allTags( Todos ts ) { 276 Tags tags; 277 foreach( t; ts ) { 278 foreach( tag; t.tags ) { 279 auto found = tags.find( tag ); 280 if (found.empty) 281 tags.add( tag ); 282 else { 283 if (tag.id.empty) { 284 t.tags.remove( tag ); 285 t.tags.add( found[0] ); 286 } else { 287 tags.remove( tag ); 288 tags.add( tag ); 289 } 290 } 291 } 292 } 293 294 return tags; 295 } 296 297 unittest { 298 auto ts = generateSomeTodos(); 299 assert( equal( ts.allTags().array, [new Tag("tag1"), new Tag("tag2"), 300 new Tag("tag3"), new Tag("tag4")] ) ); 301 } 302 303 /** 304 Number of occurences of each given tag 305 */ 306 size_t[Tag] tagOccurence(TODOS)(TODOS ts, Tags tags, Tags exclude = Tags()) { 307 size_t[Tag] tagsCounts; 308 foreach( t; ts.filter!((a) => a.tags.filter!((b) => exclude.canFind(b)).empty)) { 309 foreach( tag; t.tags ) { 310 if (tags.canFind( tag ) ) { 311 tagsCounts[tag]++; 312 } 313 } 314 if (t.tags.length == 0) 315 tagsCounts[new Tag("untagged")]++; 316 } 317 return tagsCounts; 318 } 319 320 unittest { 321 auto ts = generateSomeTodos(); 322 auto expected = ["tag2":2, "tag3":1, "tag4":1, "tag1":1]; 323 foreach( k, v; ts.tagOccurence( ts.allTags ) ) { 324 assert( v == expected[k.name] ); 325 } 326 } 327 328 /** 329 Turn Todos into a string 330 */ 331 string toString( Todos ts ) { 332 string str; 333 foreach( t; ts ) { 334 str = str ~ toString( t ) ~ "\n"; 335 } 336 return str; 337 } 338 339 /** 340 Turn Todos into a JSONValue 341 */ 342 JSONValue toJSON( Todos ts ) { 343 JSONValue[] jsonTODOS; 344 foreach (t; ts) 345 jsonTODOS ~= toJSON( t ); 346 JSONValue[string] json; 347 json["todos"] = jsonTODOS; 348 return JSONValue( json ); 349 } 350 351 Todos toTodos( const JSONValue json ) { 352 Todos ts; 353 foreach ( js; json["todos"].array ) 354 ts.add( toTodo( js ) ); 355 return ts; 356 } 357 358 unittest { 359 auto mytodos = generateSomeTodos(); 360 assert( toJSON( mytodos ).toTodos.array[0] == mytodos.array[0] ); 361 } 362 363 Todos loadTodos( GitRepo gr ) { 364 Todos ts; 365 auto todosFileName = "todos.json"; 366 auto content = readFile( gr.workPath, todosFileName ); 367 if (content != "") 368 ts = toTodos( parseJSON( content ) ); 369 return ts; 370 } 371 372 Todos loadTodos( GitRepo gr, Tags tags ) { 373 Todos todos; 374 auto todosFileName = "todos.json"; 375 auto content = readFile( gr.workPath, todosFileName ); 376 if (content != "") 377 todos = jsonToSet!(Todo)( std.json.parseJSON( content )["todos"], 378 (js) => toTodo( js, tags ) ); 379 return todos; 380 } 381 382 void writeTodos( Todos ts, GitRepo gr ) { 383 auto todosFileName = "todos.json"; 384 writeToFile( gr.workPath, todosFileName, toJSON( ts ).toPrettyString ); 385 commitChanges( gr, todosFileName, "Updating todos file" ); 386 }