Permalink
Browse files

ranges are only enumerated explicitly when necessary

Until now, when a (discrete) TRange is created, every element of the
range is enumerated and appended to the value list on creation.
This takes a long time when the range is very big, and you might not
even use the enumeration - `random(range)`, for example, can pick at
random using arithmetic.

Now, TRange.value is just the list [start,end,step_size], and ranges are
converted to lists when necessary - it's up to functions operating on
ranges to do this conversion.

Convert a list to a range using math.rangeToList, and get the number of
elements of a discrete list with math.rangeSize

fixes #467
  • Loading branch information...
christianp committed Feb 1, 2017
1 parent ec7a3c8 commit 9cabaaaa42e7df17378ff99bcf86367512e89519
Showing with 83 additions and 61 deletions.
  1. +25 −25 runtime/scripts/jme-builtins.js
  2. +7 −12 runtime/scripts/jme.js
  3. +39 −18 runtime/scripts/math.js
  4. +1 −0 runtime/scripts/question.js
  5. +11 −6 tests/jme.html
@@ -165,22 +165,21 @@ newBuiltin('#', [TRange,TNum], TRange, math.rangeSteps, {doc: {usage: ['a..b#c',
newBuiltin('in',[TNum,TRange],TBool,function(x,r) {
var start = r[0];
var end = r[1];
var step = r[2];
var step_size = r[2];
if(x>end || x<start) {
return false;
}
if(step===0) {
if(step_size===0) {
return true;
} else {
var max_steps = Math.floor(end-start)/step;
var steps = Math.floor((x-start)/step);
return step*steps+start==x && steps <= max_steps;
var max_steps = Math.floor(end-start)/step_size;
var steps = Math.floor((x-start)/step_size);
return step_size*steps + start == x && steps <= max_steps;
}
});

newBuiltin('list',[TRange],TList,function(range) {
var numbers = range.slice(3).map(function(n){ return new TNum(n); });
return numbers;
return math.rangeToList(range).map(function(n){return new TNum(n)});
});

newBuiltin('dict',['*keypair'],TDict,null,{
@@ -307,16 +306,15 @@ newBuiltin('separateThousands',[TNum,TString],TString,util.separateThousands);
//exclude numbers from a range, given either as a range, a list or a single value
newBuiltin('except', [TRange,TRange], TList,
function(range,except) {
if(range[2]==0)
if(range[2]==0) {
throw(new Numbas.Error("jme.func.except.continuous range"));
range = range.slice(3);
if(except[2]==0)
{
}

range = math.rangeToList(range);
if(except[2]==0) {
return range.filter(function(i){return i<except[0] || i>except[1]}).map(function(i){return new TNum(i)});
}
else
{
except = except.slice(3);
} else {
except = math.rangeToList(except);
return math.except(range,except).map(function(i){return new TNum(i)});
}
},
@@ -330,9 +328,10 @@ newBuiltin('except', [TRange,TRange], TList,

newBuiltin('except', [TRange,TList], TList,
function(range,except) {
if(range[2]==0)
if(range[2]==0) {
throw(new Numbas.Error("jme.func.except.continuous range"));
range = range.slice(3);
}
range = math.rangeToList(range)
except = except.map(function(i){ return i.value; });
return math.except(range,except).map(function(i){return new TNum(i)});
},
@@ -346,9 +345,10 @@ newBuiltin('except', [TRange,TList], TList,

newBuiltin('except', [TRange,TNum], TList,
function(range,except) {
if(range[2]==0)
if(range[2]==0) {
throw(new Numbas.Error("jme.func.except.continuous range"));
range = range.slice(3);
}
range = math.rangeToList(range);
return math.except(range,[except]).map(function(i){return new TNum(i)});
},

@@ -363,7 +363,7 @@ newBuiltin('except', [TRange,TNum], TList,
newBuiltin('except', [TList,TRange], TList,
function(range,except) {
range = range.map(function(i){ return i.value; });
except = except.slice(3);
except = math.rangeToList(except);
return math.except(range,except).map(function(i){return new TNum(i)});
},

@@ -441,7 +441,7 @@ newBuiltin('implies', [TBool,TBool], TBool, function(a,b){return !a || b;}, {doc
newBuiltin('abs', [TNum], TNum, math.abs, {doc: {usage: 'abs(x)', description: 'Absolute value of a number.', tags: ['norm','length','complex']}} );
newBuiltin('abs', [TString], TNum, function(s){return s.length}, {doc: {usage: 'abs(x)', description: 'Absolute value of a number.', tags: ['norm','length','complex']}} );
newBuiltin('abs', [TList], TNum, function(l) { return l.length; }, {doc: {usage: 'abs([1,2,3])', description: 'Length of a list.', tags: ['size','number','elements']}});
newBuiltin('abs', [TRange], TNum, function(r) { return r[2]==0 ? Math.abs(r[0]-r[1]) : r.length-3; }, {doc: {usage: 'abs(1..5)', description: 'Number of elements in a numerical range.', tags: ['size','length']}});
newBuiltin('abs', [TRange], TNum, function(r) { return r[2]==0 ? Math.abs(r[0]-r[1]) : math.rangeSize(r); }, {doc: {usage: 'abs(1..5)', description: 'Number of elements in a numerical range.', tags: ['size','length']}});
newBuiltin('abs', [TVector], TNum, vectormath.abs, {doc: {usage: 'abs(vector(1,2,3))', description: 'Modulus of a vector.', tags: ['size','length','norm']}});
newBuiltin('abs', [TDict], TNum, function(d) {
var n = 0;
@@ -610,7 +610,7 @@ newBuiltin('shuffle',[TList],TList,

newBuiltin('shuffle',[TRange],TList,
function(range) {
var list = range.slice(3).map(function(n){return new TNum(n)})
var list = math.rangeToList(range).map(function(n){return new TNum(n)})
return math.shuffle(list);
},
{
@@ -928,7 +928,7 @@ jme.mapFunctions = {
'list': mapOverList,
'set': mapOverList,
'range': function(lambda,name,range,scope) {
var list = range.slice(3).map(function(n){return new TNum(n)});
var list = math.rangeToList(range).map(function(n){return new TNum(n)});
return mapOverList(lambda,name,list,scope);
},
'matrix': function(lambda,name,matrix,scope) {
@@ -1009,7 +1009,7 @@ newBuiltin('filter',['?',TName,'?'],TList,null, {
list = list.value;
break;
case 'range':
list = list.value.slice(3);
list = math.rangeToList(list.value);
for(var i=0;i<list.length;i++) {
list[i] = new TNum(list[i]);
}
@@ -1152,7 +1152,7 @@ newBuiltin('set',[TList],TSet,function(l) {
return util.distinct(l);
});
newBuiltin('set',[TRange],TSet,function(r) {
return r.slice(3).map(function(n){return new TNum(n)});
return math.rangeToList(r).map(function(n){return new TNum(n)});
});

newBuiltin('set', ['?'], TSet, null, {
@@ -1589,6 +1589,9 @@ TMatrix.doc = {
* @augments Numbas.jme.token
* @property {number[]} value - `[start,end,step]` and then, if the range is discrete, all the values included in the range.
* @property {number} size - the number of values in the range (if it's discrete, `undefined` otherwise)
* @property {number} start - the lower bound of the range
* @property {number} end - the upper bound of the range
* @property {number} start - the difference between elements in the range
* @property type "range"
* @constructor
* @param {number[]} range - `[start,end,step]`
@@ -1598,18 +1601,10 @@ var TRange = types.TRange = types.range = function(range)
this.value = range;
if(this.value!==undefined)
{
var start = this.value[0], end = this.value[1], step = this.value[2];

//if range is discrete, store all values in range so they don't need to be computed each time
if(step != 0)
{
var n = (end-start)/step;
this.size = n+1;
for(var i=0;i<=n;i++)
{
this.value[i+3] = start+i*step;
}
}
this.start = this.value[0];
this.end = this.value[1];
this.step = this.value[2];
this.size = Math.floor((this.end-this.start)/this.step);
}
}
TRange.prototype.type = 'range';
@@ -1279,24 +1279,13 @@ var math = Numbas.math = /** @lends Numbas.math */ {
*/
random: function(range)
{
if(range.length>3) //if values in range are given after [min,max,step]
{
return math.choose(range.slice(3));
}
else
{
if(range[2]==0)
{
return math.randomrange(range[0],range[1]);
}
else
{
var diff = range[1]-range[0];
var steps = diff/range[2];
var n = Math.floor(math.randomrange(0,steps+1));
return range[0]+n*range[2];
}
}
if(range[2]==0) {
return math.randomrange(range[0],range[1]);
} else {
var num_steps = math.rangeSize(range);
var n = Math.floor(math.randomrange(0,num_steps));
return range[0]+n*range[2];
}
},

/** Remove all the values in the list `exclude` from the list `range`
@@ -1497,6 +1486,38 @@ var math = Numbas.math = /** @lends Numbas.math */ {
return [range[0],range[1],step];
},

/** Convert a range to a list - enumerate all the elements of the range
* @param {range} range
* @returns {number[]}
*/
rangeToList: function(range) {
var start = range[0];
var end = range[1];
var step_size = range[2];
var out = [];
var n = 0;
var t = start;
while(start<end ? t<=end : t>=end)
{
out.push(t)
n += 1;
t = start + n*step_size;
}

return out;
},

/** Calculate the number of elements in a range
* @param {range} range
* @returns {number}
*/
rangeSize: function(range) {
var diff = range[1]-range[0];
var num_steps = Math.floor(diff/range[2])+1;
num_steps += (range[0]+num_steps*range[2] == range[1] ? 1 : 0);
return num_steps;
},

/** Get a rational approximation to a real number by the continued fractions method.
*
* If `accuracy` is given, the returned answer will be within `Math.exp(-accuracy)` of the original number
@@ -172,6 +172,7 @@ var Question = Numbas.Question = function( exam, group, xml, number, loading, gs
}

q.scope = new jme.Scope([q.scope]);
q.scope.flatten();

q.unwrappedVariables = {};
var all_variables = q.scope.allVariables()
@@ -273,7 +273,7 @@ <h2 id="qunit-userAgent"></h2>
test('Literals',function() {
closeEquals(evaluate('1').value,1,'1');
closeEquals(evaluate('true').value,true,'true');
deepCloseEqual(evaluate('1..3').value,[1,3,1,1,2,3],'1..3');
deepCloseEqual(evaluate('1..3').value,[1,3,1],'1..3');
deepCloseEqual(evaluate('[1,2]').value.map(getValue),[1,2],'[1,2]');
deepCloseEqual(evaluate('[1,"hi",true]').value.map(getValue),[1,"hi",true],'[1,"hi",true]');
closeEquals(evaluate('"hi"').value,'hi','"hi"');
@@ -711,11 +711,15 @@ <h2 id="qunit-userAgent"></h2>
});
test('Range operations',function() {
deepCloseEqual(evaluate('1..5').value,[1,5,1,1,2,3,4,5],'1..5');
deepCloseEqual(evaluate('1..7#2').value,[1,7,2,1,3,5,7],'1..#7#2');
deepCloseEqual(evaluate('-2..3#2').value,[-2,3,2,-2,0,2],'-2..3#2');
deepCloseEqual(evaluate('100..102#1/3').value,[100,102,1/3,100,100+1/3,100+2/3,101,101+1/3,101+2/3,102],'100..102#1/3');
deepCloseEqual(evaluate('6..1#-1').value,[6,1,-1,6,5,4,3,2,1],'6..1#-1');
deepCloseEqual(evaluate('1..5').value,[1,5,1],'1..5');
deepCloseEqual(jme.unwrapValue(evaluate('list(1..5)')),[1,2,3,4,5],'list(1..5)');
deepCloseEqual(evaluate('1..7#2').value,[1,7,2],'1..#7#2');
deepCloseEqual(jme.unwrapValue(evaluate('list(1..7#2)')),[1,3,5,7],'list(1..#7#2)');
deepCloseEqual(evaluate('-2..3#2').value,[-2,3,2],'-2..3#2');
deepCloseEqual(jme.unwrapValue(evaluate('list(-2..3#2)')),[-2,0,2],'list(-2..3#2)');
deepCloseEqual(jme.unwrapValue(evaluate('list(100..102#1/3)')),[100,100+1/3,100+2/3,101,101+1/3,101+2/3,102],'list(100..102#1/3)');
deepCloseEqual(jme.unwrapValue(evaluate('list(6..1#-1)')),[6,5,4,3,2,1],'list(6..1#-1)');
deepCloseEqual(evaluate('1..2#0').value,[1,2,0],'1..2#0');
deepCloseEqual(evaluate('-3..7 except 0..3').value.map(getValue),[-3,-2,-1,4,5,6,7],'-3..7 except 0..3');
@@ -740,6 +744,7 @@ <h2 id="qunit-userAgent"></h2>
test('List operations',function() {
deepCloseEqual(evaluate('["a","b","c"] except "a"').value.map(getValue),['b','c'],'["a","b","c"] except "a"');
deepCloseEqual(evaluate('["a","b","c"] except ["a","c","f"]').value.map(getValue),['b'],'["a","b","c"] except ["a","c","f"]');
deepCloseEqual(evaluate('["a","b","c","d","e"][0..2]').value.map(getValue),['a','b'],'["a","b","c","d","e"][0..2]');
});
test('Dictionaries',function() {

0 comments on commit 9cabaaa

Please sign in to comment.