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; /// id is mainly used for syncing with habitrpg 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 double weight = 1; /// Weight/priority of this Todo 63 64 private this() {} 65 66 this( string tle ) { 67 auto date_tup = parseAndRemoveDueDate( tle ); 68 due_date = date_tup[0]; 69 70 creation_date = Date.now; 71 mytitle = date_tup[1]; 72 73 id = randomUUID; 74 } 75 76 @property string title() const { 77 return mytitle; 78 } 79 80 override bool opEquals(Object t) const { 81 auto td = cast(Todo)(t); 82 return mytitle == td.mytitle; 83 } 84 85 override int opCmp(Object t) const { 86 auto td = cast(Todo)(t); 87 if ( this == td ) 88 return 0; 89 else if ( title < td.title ) 90 return -1; 91 return 1; 92 } 93 94 private: 95 string mytitle; 96 } 97 98 unittest { 99 Todo t1 = new Todo( "Todo 1" ); 100 assert( t1.title == "Todo 1" ); 101 assert( !t1.id.empty ); 102 103 assert( t1.weight == 1 ); 104 } 105 106 string toString( Todo t ) { 107 string str = t.title ~ " [ "; 108 foreach ( tag; t.tags ) 109 str ~= tag.name ~ ", "; 110 str ~= "]"; 111 return str; 112 } 113 114 JSONValue toJSON( Todo t ) { 115 JSONValue[string] jsonTODO; 116 jsonTODO["title"] = t.title; 117 JSONValue[] tags; 118 foreach( tag; t.tags ) 119 tags ~= tag.toJSON(); 120 jsonTODO["tags"] = JSONValue(tags); 121 122 string[] progress_array; 123 foreach( d; t.progress ) 124 progress_array ~= d.toStringDate; 125 jsonTODO["progress"] = progress_array; 126 jsonTODO["creation_date"] = t.creation_date.toStringDate; 127 jsonTODO["due_date"] = t.due_date.toStringDate; 128 jsonTODO["id"] = t.id.toString; 129 jsonTODO["weight"] = t.weight; 130 return JSONValue( jsonTODO ); 131 } 132 133 Todo toTodo( const JSONValue json ) { 134 Todo t = new Todo(); 135 const JSONValue[string] jsonAA = json.object; 136 t.mytitle = jsonAA["title"].str; 137 foreach ( tag; jsonAA["tags"].array ) 138 t.tags.add( Tag.parseJSON( tag ) ); 139 foreach ( js; jsonAA["progress"].array ) 140 t.progress ~= Date( js.str ); 141 t.creation_date = Date( jsonAA["creation_date"].str ); 142 if ("due_date" in jsonAA) 143 t.due_date = Date( jsonAA["due_date"].str ); 144 if ("id" in jsonAA) 145 t.id = UUID( jsonAA["id"].str ); 146 if ("weight" in jsonAA) { 147 if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 148 t.weight = jsonAA["weight"].floating; 149 else 150 t.weight = cast(double)(jsonAA["weight"].integer); 151 } 152 return t; 153 } 154 155 Todo toTodo( const JSONValue json, Tags tags ) { 156 Todo t = new Todo(); 157 const JSONValue[string] jsonAA = json.object; 158 t.mytitle = jsonAA["title"].str; 159 foreach ( tag; jsonAA["tags"].array ) { 160 auto newTag = Tag.parseJSON( tag ); 161 auto found = tags.find( newTag ); 162 if (found.length>0) 163 t.tags.add( found[0] ); 164 } 165 foreach ( js; jsonAA["progress"].array ) 166 t.progress ~= Date( js.str ); 167 t.creation_date = Date( jsonAA["creation_date"].str ); 168 if ("due_date" in jsonAA) 169 t.due_date = Date( jsonAA["due_date"].str ); 170 if ("id" in jsonAA) 171 t.id = UUID( jsonAA["id"].str ); 172 if ("weight" in jsonAA) { 173 if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 174 t.weight = jsonAA["weight"].floating; 175 else 176 t.weight = cast(double)(jsonAA["weight"].integer); 177 } 178 return t; 179 } 180 181 unittest { 182 Todo t1 = new Todo( "Todo 1 +tag" ); 183 assert( toJSON( t1 ).toTodo == t1 ); 184 185 string missingDate = "{\"title\":\"Todo 1\",\"tags\":[{\"name\":\"tag\",\"id\":\"00000000-0000-0000-0000-000000000000\"}],\"progress\":[],\"creation_date\":\"2014-05-06\"}"; 186 187 assert( toTodo( parseJSON(missingDate) ).title == "Todo 1" ); 188 } 189 190 /// Days since last progress. If no progress has been made then days since creation 191 auto lastProgress( const Todo t ) { 192 Date currentDate = Date.now; 193 if ( t.progress.length > 0 ) 194 return currentDate.substract( t.progress[$-1] ); 195 else 196 return currentDate.substract( t.creation_date ); 197 } 198 199 /** 200 Working on list of todos 201 */ 202 alias Todos = Set!Todo; 203 version(unittest) { 204 Todos generateSomeTodos() { 205 Todo t1 = new Todo( "Todo 1" ); 206 t1.tags.add( [ new Tag( "tag1" ),new Tag( "tag2" ), new Tag( "tag3" ) ] ); 207 Todo t2 = new Todo( "Bla" ); 208 t2.tags.add( [ new Tag( "tag2" ), new Tag( "tag4" ) ] ); 209 Todos mytodos; 210 mytodos.add( [t2,t1] ); 211 return mytodos; 212 } 213 } 214 215 unittest { 216 auto ts = generateSomeTodos; 217 assert( ts[1].tags.length == 3 ); 218 ts[1].tags.add( new Tag( "tag5" ) ); 219 assert( ts[1].tags.length == 4 ); 220 } 221 222 /** 223 Select a weighted random set of Todos 224 */ 225 Todo[] random( Todos ts, Tags allTags, TagDelta selected, 226 string searchString, 227 in Dependencies deps, 228 in double[string] defaultWeights, size_t no = 5 ) { 229 if (ts.length > no) { 230 return randomGillespie( ts, allTags, selected, searchString, deps, 231 defaultWeights, no ); 232 } 233 return ts.array; 234 } 235 236 unittest { 237 auto mytodos = generateSomeTodos().array; 238 assert( mytodos[0].title == "Bla" ); 239 assert( mytodos[1].title == "Todo 1" ); 240 } 241 242 /// Return all existing tags 243 Tags allTags( Todos ts ) { 244 Tags tags; 245 foreach( t; ts ) { 246 foreach( tag; t.tags ) { 247 auto found = tags.find( tag ); 248 if (found.empty) 249 tags.add( tag ); 250 else { 251 if (tag.id.empty) { 252 t.tags.remove( tag ); 253 t.tags.add( found[0] ); 254 } else { 255 tags.remove( tag ); 256 tags.add( tag ); 257 } 258 } 259 } 260 } 261 262 return tags; 263 } 264 265 unittest { 266 auto ts = generateSomeTodos(); 267 assert( equal( ts.allTags().array, [new Tag("tag1"), new Tag("tag2"), 268 new Tag("tag3"), new Tag("tag4")] ) ); 269 } 270 271 /** 272 Number of occurences of each given tag 273 */ 274 size_t[Tag] tagOccurence( Todos ts, Tags tags ) { 275 size_t[Tag] tagsCounts; 276 foreach( t; ts ) { 277 foreach( tag; t.tags ) { 278 if (tags.canFind( tag ) ) { 279 tagsCounts[tag]++; 280 } 281 } 282 if (t.tags.length == 0) 283 tagsCounts[new Tag("untagged")]++; 284 } 285 return tagsCounts; 286 } 287 288 289 unittest { 290 auto ts = generateSomeTodos(); 291 auto expected = ["tag2":2, "tag3":1, "tag4":1, "tag1":1]; 292 foreach( k, v; ts.tagOccurence( ts.allTags ) ) { 293 assert( v == expected[k.name] ); 294 } 295 } 296 297 /** 298 Turn Todos into a string 299 */ 300 string toString( Todos ts ) { 301 string str; 302 foreach( t; ts ) { 303 str = str ~ toString( t ) ~ "\n"; 304 } 305 return str; 306 } 307 308 /** 309 Turn Todos into a JSONValue 310 */ 311 JSONValue toJSON( Todos ts ) { 312 JSONValue[] jsonTODOS; 313 foreach (t; ts) 314 jsonTODOS ~= toJSON( t ); 315 JSONValue[string] json; 316 json["todos"] = jsonTODOS; 317 return JSONValue( json ); 318 } 319 320 Todos toTodos( const JSONValue json ) { 321 Todos ts; 322 foreach ( js; json["todos"].array ) 323 ts.add( toTodo( js ) ); 324 return ts; 325 } 326 327 unittest { 328 auto mytodos = generateSomeTodos(); 329 assert( toJSON( mytodos ).toTodos.array[0] == mytodos.array[0] ); 330 } 331 332 Todos loadTodos( GitRepo gr ) { 333 Todos ts; 334 auto todosFileName = "todos.json"; 335 auto content = readFile( gr.workPath, todosFileName ); 336 if (content != "") 337 ts = toTodos( parseJSON( content ) ); 338 return ts; 339 } 340 341 Todos loadTodos( GitRepo gr, Tags tags ) { 342 Todos todos; 343 auto todosFileName = "todos.json"; 344 auto content = readFile( gr.workPath, todosFileName ); 345 if (content != "") 346 todos = jsonToSet!(Todo)( std.json.parseJSON( content )["todos"], 347 (js) => toTodo( js, tags ) ); 348 return todos; 349 } 350 351 void writeTodos( Todos ts, GitRepo gr ) { 352 auto todosFileName = "todos.json"; 353 writeToFile( gr.workPath, todosFileName, toJSON( ts ).toPrettyString ); 354 commitChanges( gr, todosFileName, "Updating todos file" ); 355 }