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.random; 25 26 import std.algorithm; 27 import std.conv; 28 import std.file; 29 import std.json; 30 import std.math; 31 import std.random; 32 import std.stdio; 33 34 import stochastic.gillespie; 35 36 import todod.todo; 37 import todod.date; 38 import todod.dependency; 39 import todod.search; 40 import todod.tag; 41 42 version( unittest ) { 43 import std.stdio; 44 } 45 46 47 double[string] setDefaultWeights() { 48 return [ "defaultTagWeight": 0.0, "selectedTagWeight": 12.0, 49 "deselectedTagWeight": 0.0]; 50 } 51 52 double[string] loadDefaultWeights( string fileName ) { 53 auto weights = setDefaultWeights; 54 bool needUpdate = !exists( fileName ); 55 if (!needUpdate) { 56 File file = File( fileName, "r" ); 57 string content; 58 foreach( line; file.byLine() ) 59 content ~= line; 60 if (content != "") { 61 JSONValue[string] json = parseJSON( content ).object; 62 foreach( k, v ; weights ) { 63 if ( k in json ) { 64 if (json[k].type == JSON_TYPE.INTEGER) 65 weights[k] = to!double(json[k].integer); 66 else 67 weights[k] = json[k].floating; 68 } else { 69 needUpdate = true; // Missing value in weights file 70 } 71 } 72 } else { 73 needUpdate = true; 74 } 75 } 76 if (needUpdate) { 77 // Create or update incomplete config file. 78 JSONValue[string] jsonW; 79 foreach( k, v ; weights ) { 80 jsonW[k] = JSONValue( v ); 81 } 82 auto json = JSONValue( jsonW ); 83 File file = File( fileName, "w" ); 84 file.writeln( json.toPrettyString ); 85 } 86 return weights; } 87 88 /// Calculate due weight based on number of dates till due 89 auto dueWeight( long days ) { 90 double baseDays = 7; // if days == baseDays weight should return 1 91 if ( days < 0 ) 92 return 16.0; 93 else 94 return exp( (log(16.0)/baseDays) * (baseDays - to!double(days)) ); 95 } 96 97 unittest { 98 assert( dueWeight( -1 ) == 16.0 ); 99 assert( dueWeight( 8 ) < 1.0 ); 100 assert( dueWeight( 7 ) == 1.0 ); 101 assert( dueWeight( 0 ) == 16.0 ); 102 } 103 104 /// Weight due to progress 105 auto progressWeight( long days ) { 106 double max = 4.0; // days is infinite 107 double min = 0.5; // At days since last progress is 0 108 double baseDays = 7.0; // if days == baseDays weight should return 1 109 return max+(min-max)*exp(days*log( -(max-1)/(min-max) )/baseDays); 110 } 111 112 unittest { 113 assert( progressWeight( 0 ) > 0.49 ); 114 assert( progressWeight( 0 ) < 0.51 ); 115 assert( progressWeight( 7 ) > 0.99 ); 116 assert( progressWeight( 7 ) < 1.01 ); 117 assert( progressWeight( 100 ) > 3.5 && progressWeight( 100 ) < 4.0 ); 118 } 119 120 /// Weight due to tag selection 121 auto tagWeightScalar( Tags tags, TagDelta selected, 122 size_t noTodos, size_t[Tag] tagNo, in double[string] defaultWeights ) { 123 foreach ( tag; tags ) { 124 if (selected.delete_tags.canFind( tag )) 125 return defaultWeights["deselectedTagWeight"]; 126 } 127 128 double scalar = 0; 129 if (tags.length == 0 && selected.add_tags.length == 0 130 && defaultWeights["defaultTagWeight"] == 0) 131 scalar = 1; 132 foreach ( tag; tags ) { 133 // If no tags are selected and default weight is zero set tag weight to 1. This means that if nothing is selected we will get 134 // random normal flags 135 if (selected.add_tags.length == 0 && defaultWeights["defaultTagWeight"] == 0) { 136 scalar = 1; 137 } else if (selected.add_tags.canFind( tag )) { 138 if (scalar == 0) { 139 scalar = defaultWeights["selectedTagWeight"]* 140 (to!double(noTodos))/tagNo[ tag ]; 141 } else { 142 scalar = scalar*defaultWeights["selectedTagWeight"]* 143 (to!double(noTodos))/tagNo[ tag ]; 144 } 145 } 146 } 147 if (scalar == 0) 148 scalar = defaultWeights["defaultTagWeight"]; 149 150 return scalar; 151 } 152 153 unittest { 154 Tags tags; 155 TagDelta selected; 156 size_t[Tag] noTags; 157 assert( tagWeightScalar( tags, selected, 3, noTags, setDefaultWeights ) > 0 ); 158 selected.add_tags.add(new Tag("bla")); 159 assert( tagWeightScalar( tags, selected, 3, noTags, setDefaultWeights ) == 0 ); 160 } 161 162 /// Associate a weight to a Todo depending on last progress and todo dates 163 auto weight( Todo t, TagDelta selected, string searchString, 164 size_t noTodos, size_t[Tag] tagNo, in Dependencies deps, 165 in double[string] defaultWeights ) { 166 if ( deps.isAChild( t.id ) ) 167 return 0; 168 double tw = t.weight*tagWeightScalar( t.tags, selected, noTodos, tagNo, 169 defaultWeights ); 170 // Search by string; 171 tw *= pow( defaultWeights["selectedTagWeight"], weightSearchSentence( searchString, t.title ) ); 172 173 if ( t.due_date ) 174 return tw * dueWeight( t.due_date.substract( Date.now ) ); 175 return tw * progressWeight( lastProgress( t ) ); 176 } 177 178 /** 179 Randomly draw todos from the given Todo list. 180 181 Todos with a higher weight (influenced by due date, currently selected tags and 182 last progress) have a higher probability of being drawn. 183 */ 184 Todo[] randomGillespie(TODOS)(TODOS ts, Tags allTags, TagDelta selected, 185 string searchString, 186 in Dependencies deps, 187 in double[string] defaultWeights, 188 size_t no = 5 ) 189 in { 190 assert( ts.length >= no ); 191 } 192 body { 193 Todo[] selectedTodos; 194 auto gen = Random( unpredictableSeed ); 195 auto eventTodo(T)( Gillespie!(T) gillespie, Todo t, EventId id ) { 196 return { gillespie.delEvent( id ); 197 selectedTodos ~= t; }; 198 } 199 200 //Random gen = rndGen(); 201 auto gillespie = new Gillespie!(void delegate())(); 202 foreach( t; ts ) { 203 auto e_id = gillespie.newEventId; 204 gillespie.addEvent( e_id, 205 to!real( weight( t, selected, searchString, ts.length, 206 ts.tagOccurence( allTags ), deps, 207 defaultWeights ) ), 208 eventTodo( gillespie, t, e_id ) ); 209 } 210 211 if (gillespie.rate == 0) 212 return selectedTodos; 213 214 auto sim = gillespie.simulation( gen ); 215 216 for (size_t i = 0; i < no; i++) { 217 auto state = sim.front; 218 state[1](); 219 if (gillespie.rate == 0) 220 break; 221 sim.popFront; 222 } 223 224 225 return selectedTodos; 226 } 227 228 unittest { 229 Todos ts; 230 ts.add( new Todo( "Todo1" ) ); 231 ts.add( new Todo( "Todo2" ) ); 232 ts.add( new Todo( "Todo3" ) ); 233 TagDelta selected; 234 Dependencies deps; 235 assert( randomGillespie( ts, ts.allTags, selected, "", deps, 236 setDefaultWeights(), 2 ).length == 2 ); 237 }