diff --git a/.meteor/packages b/.meteor/packages index b1b39bd1..28ba06b5 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -42,6 +42,7 @@ fortawesome:fontawesome rzymek:moment-locale-nl appshore:recaptcha tmeasday:publish-counts +planner simply-debug-tools meteorhacks:kadira-debug simply:strict-reactive-var diff --git a/.meteor/versions b/.meteor/versions index 5cdc4fc6..27adf823 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -84,6 +84,7 @@ particle4dev:cheerio@0.18.0 percolate:paginated-subscription@0.2.4 percolate:synced-cron@1.2.1 percolate:velocityjs@1.2.1_1 +planner@0.0.1 qnub:emojione@0.0.3 random@1.0.3 reactive-dict@1.1.0 diff --git a/packages/planner/.gitignore b/packages/planner/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/packages/planner/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/planner/README.md b/packages/planner/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/planner/package.js b/packages/planner/package.js new file mode 100644 index 00000000..5c7e76ed --- /dev/null +++ b/packages/planner/package.js @@ -0,0 +1,30 @@ +Package.describe({ + name: 'planner', + version: '0.0.1', + // Brief, one-line summary of the package. + summary: 'The plan algorithm of simplyHomework.', + // URL to the Git repository containing the source code for this package. + git: '', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md', +}); + +Npm.depends({ + 'js-object-clone': '0.4.2', +}); + +Package.onUse(function(api) { + api.versionsFrom('1.1.0.2'); + + api.addFiles('planner.js', 'server'); + + api.export('HomeworkDescription', 'server'); + api.export('Planner', 'server'); +}); + +Package.onTest(function(api) { + api.use('tinytest'); + api.use('planner'); + api.addFiles('planner-tests.js'); +}); diff --git a/packages/planner/package.json b/packages/planner/package.json new file mode 100644 index 00000000..a9f54aee --- /dev/null +++ b/packages/planner/package.json @@ -0,0 +1,14 @@ +{ + "name": "planner", + "version": "0.0.1", + "description": "The plan algorithm of simplyHomework.", + "main": "planner.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "simply (http://www.simplyApps.nl/)", + "license": "UNLICENSED", + "dependencies": { + "js-object-clone": "^0.4.2" + } +} diff --git a/packages/planner/planner-tests.js b/packages/planner/planner-tests.js new file mode 100644 index 00000000..c5623d89 --- /dev/null +++ b/packages/planner/planner-tests.js @@ -0,0 +1,5 @@ +// Write your tests here! +// Here is an example. +Tinytest.add('example', function (test) { + test.equal(true, true); +}); diff --git a/packages/planner/planner.js b/packages/planner/planner.js new file mode 100644 index 00000000..46ea2b00 --- /dev/null +++ b/packages/planner/planner.js @@ -0,0 +1,375 @@ +/* + * simplyHomework plan algorithm and stuff. + * @author simply + * @module planner + */ + +var require = require || Npm.require; +require("js-object-clone"); +var util = require("util"); +var assert = require('assert'); + +var MIN_TIME_TASK_DAY = 450; // minimum of 7.5 minutes for one task on a day (except if expected time is <7.5 min) + +var log = function () { + //console.log.apply(console, arguments); +}; + +/** + * @class HomeworkDescription + * @constructor + * @param {any} subject Must be primitive, unique and fixed + * @param {int[][]} locations + * @param {Date} duedate + * @param {any} id + */ +HomeworkDescription=function(){ + if(!(this instanceof HomeworkDescription)){ + return new (Function.prototype.bind.apply( + HomeworkDescription, + [null].concat(Array.prototype.slice.call(arguments)) + )); + } + + /** + * A unique identifier for a subject. Can be pretty much anything, lest it's + * a primitive type and unique and fixed, i.e. there is only one identifier + * per subject and only one subject per identifier. + * + * @property subject + * @type any + */ + this.subject=arguments.length>0?arguments[0]:null; + + /** + * Items in `location` can be multiple things, e.g. + * - [chapter,paragraph,exercise] + * - [bookid,page,exercise] + * - [chapter,exercise] + * - [bookid,page] + * + * In general, anything that can be expressed in a sequence of integers that + * specifies the location. (Actually, they should be index paths — think + * NSIndexPath) The `location` property contains an array of those, because + * homework can be multiple exercises, for example. Per subject, please use + * just one index convention. If you need to, add ones where you don't have + * entries. + * + * @property location + * @type int[][] + */ + this.location=arguments.length>1?arguments[1]:[]; + + /** + * A Date indicating when this homework is due + * @prototype duedate + * @type Date + */ + this.duedate=arguments.length>2?arguments[2]:null; + + /** + * Some id that the outside world can decide on + * @property id + * @type any + */ + this.id=arguments.length>3?arguments[3]:null; +}; + +/** + * @class Planner + * @constructor + * @param {Object} [persisted] Result from `Planner::persistable`. + */ +Planner = function (persisted) { + if (!(this instanceof Planner)) { + return new Planner(persisted); + } + + /** + * @property isPlannerObject + * @type Boolean + * @final + * @default true + */ + this.isPlannerObject=true; + + //PREV: Object[subject id][location index path item][value of the loc. idxp. item][samples of time taken] + //Object[subject id][loc item 0][loc item 1]...[loc item n] = time taken for that index path + var subjects={}; //The Cache + + /** + * Learn. + * @method learn + * @param {HomeworkDescription} hwdesc The `HomeworkDescription` that was done. + * @param {Number} timetaken Number in time unit that specifies how long the given description took. + */ + this.learn=function(hwdesc,timetaken){ + var i,j,idx,subj,loc; + assert(hwdesc instanceof HomeworkDescription); + subj=hwdesc.subject; + loc=hwdesc.location; + assert(loc.length!=0); + if(!subjects[subj]){ + subjects[subj]={}; + } + var itemref; + for(i=0;i=availableCache.length){ + targetday=addDays(today,availableCache.length); + availableCache.push(availableFn(targetday)); + } + return availableCache[day]; + }).bind(this); + items=Object.clone(items,true); //we'll modify them for our own needs + var needed=new Array(items.length); + var i; + for(i=0;i planned on day "+day); + itemcopy=Object.clone(it.item,true); + itemcopy.timepart=it.needed; + itemcopy.timefraction=1; + schedule[day].push(itemcopy); + daylength[day]+=it.needed; + } else { //the item didn't fit anywhere + log(" -> no fit found; distributing"); + total=0; + fractions=[]; + firstUsedDay=-1; + for(day=0;day=it.needed)break; + } + if(total distributing left "+(it.needed-total)+" excess; putting on first used day"); + if(firstUsedDay==-1){ //HELP we didn't plan ANYTHING yet at all + log(" -> NO FIRST USED DAY, so just plugging everything on day 0"); + itemcopy=Object.clone(it.item,true); + itemcopy.timepart=it.needed; + itemcopy.timefraction=1; + schedule[0].push(itemcopy); + daylength[0]+=it.needed; + continue; //skip all the fraction stuff + } else { + firstDayItem=schedule[firstUsedDay][schedule[firstUsedDay].length-1]; + firstDayItem.timepart+=it.needed-total; + firstDayItem.timefraction=firstDayItem.timepart/it.needed; + } + total=it.needed; + } + //NOT REACHED IF SHIT WAS JUST THROWN ON DAY 0 DUE TO continue ABOVE + fractions.sort(function(a,b){a.timepart>b.timepart;}); //descending sort on timepart + lastDayItem=schedule[lastUsedDay][schedule[lastUsedDay].length-1]; + while(fractions[0].timepart