# numbas/Numbas

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
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 || xexcept[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
 @@ -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) { 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 @@

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 @@

}); 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 @@

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() {