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