var _ = require('lodash'), uuid = require('uuid'), Cursor; /** * @param {Number} [length=0] * @param {Number} [cycles=1] * @param {Number} [position=0] * @param {Number} [iteration=0] * @param {String} [ref] * @constructor */ Cursor = function RunCursor (length, cycles, position, iteration, ref) { // eslint-disable-line func-name-matching this.length = Cursor.validate(length, 0); this.position = Cursor.validate(position, 0, this.length); this.cycles = Cursor.validate(cycles, 1, 1); this.iteration = Cursor.validate(iteration, 0, this.cycles); this.ref = ref || uuid.v4(); }; _.assign(Cursor.prototype, { /** * * * @param {Object} state * @param {Number} [state.length=0] * @param {Number} [state.cycles=1] * @param {Number} [state.position=0] * @param {Number} [state.iteration=0] * @param {String} [state.ref] * @param {Function} [callback] - receives `(err:Error, coords:Object, previous:Object)` * @param {Object} [scope] */ load: function (state, callback, scope) { !state && (state = {}); (state instanceof Cursor) && (state = state.current()); this.reset(state.length, state.cycles, state.position, state.iteration, state.ref, callback, scope); }, /** * Update length and cycle bounds * * @param {Number} [length=0] * @param {Number} [cycles=1] * @param {Number} [position=0] * @param {Number} [iteration=0] * @param {String} [ref] * @param {Function} [callback] - receives `(err:Error, coords:Object, previous:Object)` * @param {Object} [scope] */ reset: function (length, cycles, position, iteration, ref, callback, scope) { var coords = _.isFunction(callback) && this.current(); // validate parameter defaults _.isNil(length) && (length = this.length); _.isNil(cycles) && (cycles = this.cycles); _.isNil(position) && (position = this.position); _.isNil(iteration) && (iteration = this.iteration); _.isNil(ref) && (ref = this.ref); // use the constructor to set the values Cursor.call(this, length, cycles, position, iteration, ref); // send before and after values to the callback return coords && callback.call(scope || this, null, this.current(), coords); }, /** * Update length and cycle bounds * * @param {Number} [length=0] * @param {Number} [cycles=1] * @param {Function} [callback] - receives `(err:Error, coords:Object, previous:Object)` * @param {Object} [scope] */ bounds: function (length, cycles, callback, scope) { var coords = _.isFunction(callback) && this.current(); // validate parameter defaults _.isNil(length) && (length = this.length); _.isNil(cycles) && (cycles = this.cycles); // use the constructor to set the values Cursor.call(this, length, cycles, this.position, this.iteration); return coords && callback.call(scope || this, null, this.current(), coords); }, /** * Set everything to minimum dimension * * @param {Function} [callback] - receives `(err:Error, coords:Object, previous:Object)` * @param {Object} [scope] */ zero: function (callback, scope) { var coords = _.isFunction(callback) && this.current(); this.position = 0; this.iteration = 0; // send before and after values to the callback return coords && callback.call(scope || this, null, this.current(), coords); }, /** * Set everything to mnimum dimension * * @param {Function} [callback] - receives `(err:Error, coords:Object, previous:Object)` * @param {Object} [scope] */ clear: function (callback, scope) { var coords = _.isFunction(callback) && this.current(); this.position = 0; this.iteration = 0; this.cycles = 1; this.length = 0; return coords && callback.call(scope || this, null, this.current(), coords); }, /** * Seek to a specified Cursor * * @param {Number} [position] * @param {Number} [iteration] * @param {Function} [callback] - receives `(err:Error, changed:Boolean, coords:Object, previous:Object)` * @param {Object} [scope] */ seek: function (position, iteration, callback, scope) { var coords = _.isFunction(callback) && this.current(); // if null or undefined implies use existing seek position _.isNil(position) && (position = this.position); _.isNil(iteration) && (iteration = this.iteration); // make the pointers stay within boundary if ((position >= this.length) || (iteration >= this.cycles) || (position < 0) || (iteration < 0) || isNaN(position) || isNaN(iteration)) { return coords && callback.call(scope || this, new Error('runcursor: seeking out of bounds: ' + [position, iteration])); } // floor the numbers position = ~~position; iteration = ~~iteration; // set the new positions this.position = Cursor.validate(position, 0, this.length); this.iteration = Cursor.validate(iteration, 0, this.cycles); // finally execute the callback with the seek position return coords && callback.call(scope || this, null, this.hasChanged(coords), this.current(), coords); }, /** * Seek one forward * * @param {Function} [callback] - receives `(err:Error, changed:Boolean, coords:Object, previous:Object)` * @param {Object} [scope] */ next: function (callback, scope) { var position = this.position, iteration = this.iteration, coords; // increment position position += 1; // check if we need to increment cycle if (position >= this.length) { // set position to 0 and increment iteration position = 0; iteration += 1; if (iteration >= this.cycles) { coords = _.isFunction(callback) && this.current(); coords.eof = true; return coords && callback.call(scope || this, null, false, coords, coords); } coords && (coords.cr = true); } // finally handover the new coordinates to seek function return this.seek(position, iteration, callback, scope); }, /** * Tentative Cursor status, if we do `.next()` * * @param {Object} coords * * @returns {Object} */ whatnext: function (coords) { var base = { ref: this.ref, length: this.length, cycles: this.cycles }, position, iteration; if (!_.isObject(coords)) { return _.assign(base, {eof: true, bof: true, empty: this.empty()}); } if (!this.length) { return _.assign(base, {eof: true, bof: true, empty: true}); } position = coords.position; iteration = coords.iteration; // increment position position += 1; // check if we need to increment cycle if (position >= this.length) { // set position to 0 and increment iteration position = 0; iteration += 1; if (iteration >= this.cycles) { return _.assign(base, { position: this.length - 1, iteration: iteration - 1, eof: true }); } return _.assign(base, { position: position, iteration: iteration, cr: true }); } return _.assign(base, {position: position, iteration: iteration}); }, /** * Check whether current position and iteration is not as the same specified * * @param {Object} coords * @returns {Boolean} */ hasChanged: function (coords) { return _.isObject(coords) && !((this.position === coords.position) && (this.iteration === coords.iteration)); }, /** * Current Cursor state * * @returns {Object} */ current: function () { return { position: this.position, iteration: this.iteration, length: this.length, cycles: this.cycles, empty: this.empty(), eof: this.eof(), bof: this.bof(), cr: this.cr(), ref: this.ref }; }, /** * Is the current position going to trigger a new iteration on `.next`? * * @returns {Boolean} */ cr: function () { return !this.length || (this.position >= this.length); }, /** * @returns {Boolean} */ eof: function () { return !this.length || (this.position >= this.length) && (this.iteration >= this.cycles); }, /** * @returns {Boolean} */ bof: function () { return !this.length || ((this.position === 0) && (this.iteration === 0)); }, /** * @returns {Boolean} */ empty: function () { return !this.length; }, /** * @returns {Object} */ valueOf: function () { return this.current(); }, clone: function () { return new Cursor(this.length, this.cycles, this.position, this.iteration); } }); _.assign(Cursor, { /** * @param {Number} [length=0] * @param {Number} [cycles=1] * @param {Number} [position=0] * @param {Number} [iteration=0] * @param {String} [ref] * * @returns {Number} */ create: function (length, cycles, position, iteration, ref) { return new Cursor(length, cycles, position, iteration, ref); }, /** * @param {Object|Cursor} obj * @param {Object} [bounds] * @param {Number} [bounds.length] * @param {Number} [bounds.cycles] * * @returns {Cursor} */ box: function (obj, bounds) { // already a Cursor, do nothing if (obj instanceof Cursor) { bounds && obj.bounds(bounds.length, bounds.cycles); return obj; } // nothing to box, create a blank Cursor if (!_.isObject(obj)) { return new Cursor(bounds && bounds.length, bounds && bounds.cycles); } // load Cursor values from object return new Cursor((bounds || obj).length, (bounds || obj).cycles, obj.position, obj.iteration, obj.ref); }, /** * @private * * @param {Number} num * @param {Number} min [description] * @param {Number} [max] * * @returns {Number} */ validate: function (num, min, max) { if (typeof num !== 'number' || num < min) { return min; } if (num === Infinity) { return _.isNil(max) ? min : max; } return num; } }); module.exports = Cursor;