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 }