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 }