From 421db6ca775e0bbecd176832a718c3e2eb474cb2 Mon Sep 17 00:00:00 2001 From: Celeste Glavin Date: Tue, 26 Aug 2025 14:29:27 -0400 Subject: [PATCH] feat: floor the frame count --- smpte-timecode.js | 492 +++++++++++++++++---------------- test/smpte-timecode-test.js | 529 ++++++++++++++++++++---------------- 2 files changed, 555 insertions(+), 466 deletions(-) diff --git a/smpte-timecode.js b/smpte-timecode.js index aeee0c0..fcd4821 100644 --- a/smpte-timecode.js +++ b/smpte-timecode.js @@ -1,258 +1,280 @@ // This should work both in node and in the browsers, so that's what this wrapper is about -;(function(root, undefined) { - // An arbitrary "close enough" number to check if the frame rate is 29.97 or - // 59.94 - var DROP_FRAME_EPSILON = 0.0001; +(function (root, undefined) { + // An arbitrary "close enough" number to check if the frame rate is 29.97 or + // 59.94 + var DROP_FRAME_EPSILON = 0.0001; - /** - * Timecode object constructor - * @param {number|String|Date|Object} timeCode Frame count as number, "HH:MM:SS(:|;|.)FF", Date(), or object. - * @param {number} frameRate Frame rate - * @param {boolean} [dropFrame=true] Whether the timecode is drop-frame or not - * @constructor - * @returns {Timecode} timecode - */ - var Timecode = function ( timeCode, frameRate, dropFrame ) { + /** + * Timecode object constructor + * @param {number|String|Date|Object} timeCode Frame count as number, "HH:MM:SS(:|;|.)FF", Date(), or object. + * @param {number} frameRate Frame rate + * @param {boolean} [dropFrame=true] Whether the timecode is drop-frame or not + * @constructor + * @returns {Timecode} timecode + */ + var Timecode = function (timeCode, frameRate, dropFrame) { + // Make this class safe for use without "new" + if (!(this instanceof Timecode)) + return new Timecode(timeCode, frameRate, dropFrame); - // Make this class safe for use without "new" - if (!(this instanceof Timecode)) return new Timecode( timeCode, frameRate, dropFrame); + // Get frame rate + if (typeof frameRate === "number" && frameRate > 0) + this.frameRate = frameRate; + else throw new Error("Number expected as framerate"); - // Get frame rate - if (typeof frameRate === 'number' && frameRate>0) this.frameRate = frameRate; - else throw new Error('Number expected as framerate'); + // If we are passed dropFrame, we need to use it + if (typeof dropFrame === "boolean") this.dropFrame = dropFrame; + // Otherwise, default to DF for 29.97 and 59.94 + else this.dropFrame = this._is30DF() || this._is60DF(); - // If we are passed dropFrame, we need to use it - if (typeof dropFrame === 'boolean') this.dropFrame = dropFrame; - // Otherwise, default to DF for 29.97 and 59.94 - else this.dropFrame = this._is30DF() || this._is60DF(); - - // Now either get the frame count, string or datetime - if (typeof timeCode === 'number') { - this.frameCount = Math.round(timeCode); - this._frameCountToTimeCode(); - } - else if (typeof timeCode === 'string') { - // pick it apart - var parts = timeCode.match('^([012]\\d)[:;](\\d\\d)[:;](\\d\\d)(:|;|\\.)(\\d\\d)$'); - if (!parts) throw new Error("Timecode string expected as HH:MM:SS:FF or HH:MM:SS;FF or HH;MM;SS;FF"); - this.hours = parseInt(parts[1]); - this.minutes = parseInt(parts[2]); - this.seconds = parseInt(parts[3]); - // do not override input parameters - if (typeof dropFrame !== 'boolean') { - this.dropFrame = parts[4]!==':'; - } - this.frames = parseInt(parts[5]); - this._timeCodeToFrameCount(); - } - else if (typeof timeCode === 'object' && timeCode instanceof Date) { - var midnight = new Date(timeCode.getFullYear(), timeCode.getMonth(), timeCode.getDate(),0,0,0); - var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000; - var timecode_tz = timeCode.getTimezoneOffset() * 60 * 1000; - this.frameCount = Math.round(((timeCode-midnight + (midnight_tz - timecode_tz))*this.frameRate)/1000); - this._frameCountToTimeCode(); - } - else if (typeof timeCode === 'object' && typeof (timeCode.hours) != 'undefined') { - this.hours = timeCode.hours; - this.minutes = timeCode.minutes; - this.seconds = timeCode.seconds; - this.frames = timeCode.frames; - this._timeCodeToFrameCount(); - } - else if (typeof timeCode === 'undefined') { - this.frameCount = 0; - } - else { - throw new Error('Timecode() constructor expects a number, timecode string, or Date()'); - } - - this._validate(timeCode); - - return this; - }; - - /** - * Validates timecode - * @private - * @param {number|String|Date|Object} timeCode for the reference - */ - Timecode.prototype._validate = function (timeCode) { + // Now either get the frame count, string or datetime + if (typeof timeCode === "number") { + this.frameCount = Math.floor(timeCode); + this._frameCountToTimeCode(); + } else if (typeof timeCode === "string") { + // pick it apart + var parts = timeCode.match( + "^([012]\\d)[:;](\\d\\d)[:;](\\d\\d)(:|;|\\.)(\\d\\d)$" + ); + if (!parts) + throw new Error( + "Timecode string expected as HH:MM:SS:FF or HH:MM:SS;FF or HH;MM;SS;FF" + ); + this.hours = parseInt(parts[1]); + this.minutes = parseInt(parts[2]); + this.seconds = parseInt(parts[3]); + // do not override input parameters + if (typeof dropFrame !== "boolean") { + this.dropFrame = parts[4] !== ":"; + } + this.frames = parseInt(parts[5]); + this._timeCodeToFrameCount(); + } else if (typeof timeCode === "object" && timeCode instanceof Date) { + var midnight = new Date( + timeCode.getFullYear(), + timeCode.getMonth(), + timeCode.getDate(), + 0, + 0, + 0 + ); + var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000; + var timecode_tz = timeCode.getTimezoneOffset() * 60 * 1000; + this.frameCount = Math.floor( + ((timeCode - midnight + (midnight_tz - timecode_tz)) * this.frameRate) / + 1000 + ); + this._frameCountToTimeCode(); + } else if ( + typeof timeCode === "object" && + typeof timeCode.hours != "undefined" + ) { + this.hours = timeCode.hours; + this.minutes = timeCode.minutes; + this.seconds = timeCode.seconds; + this.frames = timeCode.frames; + this._timeCodeToFrameCount(); + } else if (typeof timeCode === "undefined") { + this.frameCount = 0; + } else { + throw new Error( + "Timecode() constructor expects a number, timecode string, or Date()" + ); + } - // Make sure dropFrame is only for 29.97 & 59.94 - if (this.dropFrame && !this._is30DF() && !this._is60DF()) { - throw new Error('Drop frame is only supported for 29.97 and 59.94 fps'); - } + this._validate(timeCode); - // make sure the numbers make sense - if (this.hours > 23 || this.minutes > 59 || this.seconds > 59 || this.frames >= this.frameRate || - (this.dropFrame && this.seconds === 0 && this.minutes % 10 && this.frames < this._numOfFramesToDrop())) { - throw new Error("Invalid timecode" + JSON.stringify(timeCode)); - } - }; + return this; + }; - /** - * Calculate timecode based on frame count - * @private - */ - Timecode.prototype._frameCountToTimeCode = function() { - var fc = this.frameCount; - // adjust for dropFrame - if (this.dropFrame) { - var df = this._numOfFramesToDrop(); - var d = Math.floor(this.frameCount / (17982*df/2)); - var m = this.frameCount % (17982*df/2); - if (m 23 || + this.minutes > 59 || + this.seconds > 59 || + this.frames >= this.frameRate || + (this.dropFrame && + this.seconds === 0 && + this.minutes % 10 && + this.frames < this._numOfFramesToDrop()) + ) { + throw new Error("Invalid timecode" + JSON.stringify(timeCode)); + } + }; - /** - * Is the frame rate 29.97 (30000/1001) or close enough to it - * @private - */ - Timecode.prototype._is30DF = function () { - return Math.abs(30000/1001 - this.frameRate) < DROP_FRAME_EPSILON; - }; + /** + * Calculate timecode based on frame count + * @private + */ + Timecode.prototype._frameCountToTimeCode = function () { + var fc = this.frameCount; + // adjust for dropFrame + if (this.dropFrame) { + var df = this._numOfFramesToDrop(); + var d = Math.floor(this.frameCount / ((17982 * df) / 2)); + var m = this.frameCount % ((17982 * df) / 2); + if (m < df) m = m + df; + fc += 9 * df * d + df * Math.floor((m - df) / ((1798 * df) / 2)); + } + var fps = Math.round(this.frameRate); + this.frames = fc % fps; + this.seconds = Math.floor(fc / fps) % 60; + this.minutes = Math.floor(fc / (fps * 60)) % 60; + this.hours = Math.floor(fc / (fps * 3600)) % 24; + }; - /** - * Is the frame rate 59.94 (60000/1001) or close enough to it - * @private - */ - Timecode.prototype._is60DF = function () { - return Math.abs(60000/1001 - this.frameRate) < DROP_FRAME_EPSILON; - }; + /** + * Calculate frame count based on time Timecode + * @private + */ + Timecode.prototype._timeCodeToFrameCount = function () { + this.frameCount = + (this.hours * 3600 + this.minutes * 60 + this.seconds) * + Math.round(this.frameRate) + + this.frames; + // adjust for dropFrame + if (this.dropFrame) { + var totalMinutes = this.hours * 60 + this.minutes; + var df = this._numOfFramesToDrop(); + this.frameCount -= df * (totalMinutes - Math.floor(totalMinutes / 10)); + } + }; - /** - * Get the number of frames to drop — 29.97 skips 2 frames, 59.94 skips 4 - * frames. - * @private - */ - Timecode.prototype._numOfFramesToDrop = function () { - if (this._is30DF()) return 2; - else if (this._is60DF()) return 4; - else return 0; - }; + /** + * Is the frame rate 29.97 (30000/1001) or close enough to it + * @private + */ + Timecode.prototype._is30DF = function () { + return Math.abs(30000 / 1001 - this.frameRate) < DROP_FRAME_EPSILON; + }; - /** - * Convert Timecode to String - * @param {String} format output format - * @returns {string} timecode - */ - Timecode.prototype.toString = function TimeCodeToString(format) { - var frames = this.frames; - var field = ''; - if (typeof format === 'string') { - if (format === 'field') { - if (this.frameRate<=30) field = '.0'; - else { - frames = Math.floor(frames/2); - field = '.'.concat((this.frameCount%2).toString()); - }; - } - else throw new Error('Unsupported string format'); - }; - return "".concat( - this.hours<10 ? '0' : '', - this.hours.toString(), - ':', - this.minutes<10 ? '0' : '', - this.minutes.toString(), - ':', - this.seconds<10 ? '0' : '', - this.seconds.toString(), - this.dropFrame ? ';' : ':', - frames<10 ? '0' : '', - frames.toString(), - field - ); - }; + /** + * Is the frame rate 59.94 (60000/1001) or close enough to it + * @private + */ + Timecode.prototype._is60DF = function () { + return Math.abs(60000 / 1001 - this.frameRate) < DROP_FRAME_EPSILON; + }; - /** - * @returns {Number} the frame count when Timecode() object is used as a number - */ - Timecode.prototype.valueOf = function() { - return this.frameCount; - }; + /** + * Get the number of frames to drop — 29.97 skips 2 frames, 59.94 skips 4 + * frames. + * @private + */ + Timecode.prototype._numOfFramesToDrop = function () { + if (this._is30DF()) return 2; + else if (this._is60DF()) return 4; + else return 0; + }; - /** - * Adds t to timecode, in-place (i.e. the object itself changes) - * @param {number|string|Date|Timecode} t How much to add - * @param {boolean} [negative=false] Whether we are adding or subtracting - * @param {Number} [rollOverMaxHours] allow rollovers - * @returns {Timecode} timecode - */ - Timecode.prototype.add = function (t, negative, rollOverMaxHours) { - if (typeof t === 'number') { - var newFrameCount = this.frameCount + Math.round(t) * (negative?-1:1); - if (newFrameCount<0 && rollOverMaxHours > 0) { - newFrameCount = (Math.round(this.frameRate*86400)) + newFrameCount; - if (((newFrameCount / this.frameRate) / 3600) > rollOverMaxHours) { - throw new Error('Rollover arithmetic exceeds max permitted'); - } - } - if (newFrameCount<0) { - throw new Error("Negative timecodes not supported"); - } - this.frameCount = newFrameCount; - } + /** + * Convert Timecode to String + * @param {String} format output format + * @returns {string} timecode + */ + Timecode.prototype.toString = function TimeCodeToString(format) { + var frames = this.frames; + var field = ""; + if (typeof format === "string") { + if (format === "field") { + if (this.frameRate <= 30) field = ".0"; else { - if (!(t instanceof Timecode)) t = new Timecode(t, this.frameRate, this.dropFrame); - return this.add(t.frameCount,negative,rollOverMaxHours); + frames = Math.floor(frames / 2); + field = ".".concat((this.frameCount % 2).toString()); } - this.frameCount = this.frameCount % (Math.round(this.frameRate*86400)); // wraparound 24h - this._frameCountToTimeCode(); - return this; - }; - + } else throw new Error("Unsupported string format"); + } + return "".concat( + this.hours < 10 ? "0" : "", + this.hours.toString(), + ":", + this.minutes < 10 ? "0" : "", + this.minutes.toString(), + ":", + this.seconds < 10 ? "0" : "", + this.seconds.toString(), + this.dropFrame ? ";" : ":", + frames < 10 ? "0" : "", + frames.toString(), + field + ); + }; - Timecode.prototype.subtract = function(t, rollOverMaxHours) { - return this.add(t,true,rollOverMaxHours); - }; + /** + * @returns {Number} the frame count when Timecode() object is used as a number + */ + Timecode.prototype.valueOf = function () { + return this.frameCount; + }; - /** - * Converts timecode to a Date() object - * @returns {Date} date - */ - Timecode.prototype.toDate = function() { - var ms = this.frameCount/this.frameRate*1000; - var midnight = new Date(); - midnight.setHours(0); - midnight.setMinutes(0); - midnight.setSeconds(0); - midnight.setMilliseconds(0); + /** + * Adds t to timecode, in-place (i.e. the object itself changes) + * @param {number|string|Date|Timecode} t How much to add + * @param {boolean} [negative=false] Whether we are adding or subtracting + * @param {Number} [rollOverMaxHours] allow rollovers + * @returns {Timecode} timecode + */ + Timecode.prototype.add = function (t, negative, rollOverMaxHours) { + if (typeof t === "number") { + var newFrameCount = this.frameCount + Math.floor(t) * (negative ? -1 : 1); + if (newFrameCount < 0 && rollOverMaxHours > 0) { + newFrameCount = Math.floor(this.frameRate * 86400) + newFrameCount; + if (newFrameCount / this.frameRate / 3600 > rollOverMaxHours) { + throw new Error("Rollover arithmetic exceeds max permitted"); + } + } + if (newFrameCount < 0) { + throw new Error("Negative timecodes not supported"); + } + this.frameCount = newFrameCount; + } else { + if (!(t instanceof Timecode)) + t = new Timecode(t, this.frameRate, this.dropFrame); + return this.add(t.frameCount, negative, rollOverMaxHours); + } + this.frameCount = this.frameCount % Math.floor(this.frameRate * 86400); // wraparound 24h + this._frameCountToTimeCode(); + return this; + }; - var d = new Date( midnight.valueOf() + ms ); - var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000; - var timecode_tz = d.getTimezoneOffset() * 60 * 1000; - return new Date( midnight.valueOf() + ms + (timecode_tz-midnight_tz)); - }; + Timecode.prototype.subtract = function (t, rollOverMaxHours) { + return this.add(t, true, rollOverMaxHours); + }; - // Export it for Node or attach to root for in-browser - /* istanbul ignore else */ - if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { - module.exports = Timecode; - } else if (root) { - root.Timecode = Timecode; - } + /** + * Converts timecode to a Date() object + * @returns {Date} date + */ + Timecode.prototype.toDate = function () { + var ms = (this.frameCount / this.frameRate) * 1000; + var midnight = new Date(); + midnight.setHours(0); + midnight.setMinutes(0); + midnight.setSeconds(0); + midnight.setMilliseconds(0); + var d = new Date(midnight.valueOf() + ms); + var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000; + var timecode_tz = d.getTimezoneOffset() * 60 * 1000; + return new Date(midnight.valueOf() + ms + (timecode_tz - midnight_tz)); + }; -}(this)); + // Export it for Node or attach to root for in-browser + /* istanbul ignore else */ + if (typeof exports === "object" && typeof exports.nodeName !== "string") { + module.exports = Timecode; + } else if (root) { + root.Timecode = Timecode; + } +})(this); diff --git a/test/smpte-timecode-test.js b/test/smpte-timecode-test.js index 49b3b6f..a5556a5 100644 --- a/test/smpte-timecode-test.js +++ b/test/smpte-timecode-test.js @@ -1,245 +1,312 @@ -var sinon = require('sinon'); +var sinon = require("sinon"); // If we are running under Node, we need to add expect and load our module -if (typeof module !== 'undefined' && module.exports) { - global.expect = require('expect.js'); - global.Timecode = require('../smpte-timecode.js'); +if (typeof module !== "undefined" && module.exports) { + global.expect = require("expect.js"); + global.Timecode = require("../smpte-timecode.js"); } -describe('Constructor tests', function(){ - - var t = Timecode(100,29.97); - - it ('no new still gets you Timecode()', function() { - expect(Timecode(3,29.97)).to.be.a(Timecode); - }); - - it ('numbers converted to framecounts', function() { - expect(Timecode(15,29.97).frameCount).to.be(15); - expect(Timecode(323.443,29.97).frameCount).to.be(323); - }); - - it ('incorrect initializers throw', function() { - expect(function(){Timecode('dewdew',29.97);}).to.throwException(); - expect(function(){Timecode({w:3},29.97);}).to.throwException(); - }); - - it ('string initializers work', function(){ - var t = new Timecode('12:33:44;12',29.97); - expect(t.hours).to.be(12); - expect(t.minutes).to.be(33); - expect(t.seconds).to.be(44); - expect(t.frames).to.be(12); - expect(t.dropFrame).to.be(true); - expect(t.frameRate).to.be(29.97); - t = new Timecode('12:33:44:12',29.97); - expect(t.hours).to.be(12); - expect(t.minutes).to.be(33); - expect(t.seconds).to.be(44); - expect(t.frames).to.be(12); - expect(t.dropFrame).to.be(false); - expect(t.frameRate).to.be(29.97); - }); - - it ('invalid timecodes throw', function(){ - expect(function(){Timecode('40:02:00;02')}).to.throwError(); - expect(function(){Timecode('00:99:00;02')}).to.throwError(); - expect(function(){Timecode('00:02:99;02')}).to.throwError(); - expect(function(){Timecode('00:02:00;35')}).to.throwError(); - }) - - it ('initializing from an object',function(){ - var t = new Timecode( {hours:12, minutes:34, seconds:56, frames:2 },29.97 ); - expect(t.toString()).to.be('12:34:56;02'); - }); - - it ('initialization defaults', function() { - var t = Timecode(0,29.97); - expect(t.frameCount).to.be(0); - expect(t.frameRate).to.be(29.97); - expect(t.dropFrame).to.be(true); - expect(Timecode(1,29.97).dropFrame).to.be(true); - expect(Timecode(1,30000/1001).dropFrame).to.be(true); - expect(Timecode(1,59.94).dropFrame).to.be(true); - expect(Timecode(1,60000/1001).dropFrame).to.be(true); - expect(Timecode(1,25).dropFrame).to.be(false); - }); - - it ('drop-frame only for 29.97 and 59.94', function() { - expect(function(){Timecode(0,30,true)}).to.throwException(); - expect(function(){Timecode(0,59.94,true)}).to.not.throwException(); - }); - - it ('drop-frame counts', function() { - expect(Timecode('00:10:00;00',29.97).frameCount).to.be(17982); - expect(Timecode('00:10:00;00',59.94).frameCount).to.be(17982*2); - expect(Timecode('10:00:00;00',29.97).frameCount).to.be(1078920); - expect(Timecode('10:00:00;00',59.94).frameCount).to.be(1078920*2); - expect(function(){Timecode('00:02:00;00',29.97)}).to.throwError(); - expect(function(){Timecode('00:02:00;02',29.97)}).to.not.throwError(); - expect(function(){Timecode('00:02:00;00',59.94)}).to.throwError(); - expect(function(){Timecode('00:02:00;02',59.94)}).to.throwError(); - expect(function(){Timecode('00:02:00;04',59.94)}).to.not.throwError(); - expect(Timecode('00:01:59;29',29.97).frameCount).to.be(3597); - expect(Timecode('00:01:59;59',59.94).frameCount).to.be(3597*2+1); - expect(Timecode(17982,29.97,true).toString()).to.be('00:10:00;00'); - expect(Timecode(1078920,29.97,true).toString()).to.be('10:00:00;00'); - expect(Timecode(3597,29.97,true).toString()).to.be('00:01:59;29'); - expect(Timecode(17982*2,59.94,true).toString()).to.be('00:10:00;00'); - expect(Timecode(1078920*2,59.94,true).toString()).to.be('10:00:00;00'); - expect(Timecode(3597*2+1,59.94,true).toString()).to.be('00:01:59;59'); - }); - it ('non-drop-frame counts', function() { - expect(Timecode('00:10:00:00',25).frameCount).to.be(15000); - expect(Timecode('10:00:00:00',25).frameCount).to.be(900000); - expect(Timecode('00:02:00:00',25).frameCount).to.be(3000); - expect(Timecode('00:01:59:24',25).frameCount).to.be(2999); - expect(Timecode(15000,25).toString()).to.be('00:10:00:00'); - expect(Timecode(900000,25).toString()).to.be('10:00:00:00'); - expect(Timecode(2999,25).toString()).to.be('00:01:59:24'); - }); - - it ('non-standard frame rates', function() { - expect(Timecode('00:10:00:00',28).frameCount).to.be(16800); - }); - - it ('parses semicolon-delimited timecodes', () => { - const tc = Timecode('00;10;00;23', 29.97); - expect(tc.frameCount).to.be(18005); - expect(tc.dropFrame).to.be(true); - expect(tc.toString()).to.be('00:10:00;23'); - }); +describe("Constructor tests", function () { + var t = Timecode(100, 29.97); + + it("no new still gets you Timecode()", function () { + expect(Timecode(3, 29.97)).to.be.a(Timecode); + }); + + it("numbers converted to framecounts", function () { + expect(Timecode(15, 29.97).frameCount).to.be(15); + expect(Timecode(323.443, 29.97).frameCount).to.be(323); + }); + + it("floors fractional frame counts on later edge of frame", function () { + expect(Timecode(100.8, 29.97).frameCount).to.be(100); + }); + + it("incorrect initializers throw", function () { + expect(function () { + Timecode("dewdew", 29.97); + }).to.throwException(); + expect(function () { + Timecode({ w: 3 }, 29.97); + }).to.throwException(); + }); + + it("string initializers work", function () { + var t = new Timecode("12:33:44;12", 29.97); + expect(t.hours).to.be(12); + expect(t.minutes).to.be(33); + expect(t.seconds).to.be(44); + expect(t.frames).to.be(12); + expect(t.dropFrame).to.be(true); + expect(t.frameRate).to.be(29.97); + t = new Timecode("12:33:44:12", 29.97); + expect(t.hours).to.be(12); + expect(t.minutes).to.be(33); + expect(t.seconds).to.be(44); + expect(t.frames).to.be(12); + expect(t.dropFrame).to.be(false); + expect(t.frameRate).to.be(29.97); + }); + + it("invalid timecodes throw", function () { + expect(function () { + Timecode("40:02:00;02"); + }).to.throwError(); + expect(function () { + Timecode("00:99:00;02"); + }).to.throwError(); + expect(function () { + Timecode("00:02:99;02"); + }).to.throwError(); + expect(function () { + Timecode("00:02:00;35"); + }).to.throwError(); + }); + + it("initializing from an object", function () { + var t = new Timecode( + { hours: 12, minutes: 34, seconds: 56, frames: 2 }, + 29.97 + ); + expect(t.toString()).to.be("12:34:56;02"); + }); + + it("initialization defaults", function () { + var t = Timecode(0, 29.97); + expect(t.frameCount).to.be(0); + expect(t.frameRate).to.be(29.97); + expect(t.dropFrame).to.be(true); + expect(Timecode(1, 29.97).dropFrame).to.be(true); + expect(Timecode(1, 30000 / 1001).dropFrame).to.be(true); + expect(Timecode(1, 59.94).dropFrame).to.be(true); + expect(Timecode(1, 60000 / 1001).dropFrame).to.be(true); + expect(Timecode(1, 25).dropFrame).to.be(false); + }); + + it("drop-frame only for 29.97 and 59.94", function () { + expect(function () { + Timecode(0, 30, true); + }).to.throwException(); + expect(function () { + Timecode(0, 59.94, true); + }).to.not.throwException(); + }); + + it("drop-frame counts", function () { + expect(Timecode("00:10:00;00", 29.97).frameCount).to.be(17982); + expect(Timecode("00:10:00;00", 59.94).frameCount).to.be(17982 * 2); + expect(Timecode("10:00:00;00", 29.97).frameCount).to.be(1078920); + expect(Timecode("10:00:00;00", 59.94).frameCount).to.be(1078920 * 2); + expect(function () { + Timecode("00:02:00;00", 29.97); + }).to.throwError(); + expect(function () { + Timecode("00:02:00;02", 29.97); + }).to.not.throwError(); + expect(function () { + Timecode("00:02:00;00", 59.94); + }).to.throwError(); + expect(function () { + Timecode("00:02:00;02", 59.94); + }).to.throwError(); + expect(function () { + Timecode("00:02:00;04", 59.94); + }).to.not.throwError(); + expect(Timecode("00:01:59;29", 29.97).frameCount).to.be(3597); + expect(Timecode("00:01:59;59", 59.94).frameCount).to.be(3597 * 2 + 1); + expect(Timecode(17982, 29.97, true).toString()).to.be("00:10:00;00"); + expect(Timecode(1078920, 29.97, true).toString()).to.be("10:00:00;00"); + expect(Timecode(3597, 29.97, true).toString()).to.be("00:01:59;29"); + expect(Timecode(17982 * 2, 59.94, true).toString()).to.be("00:10:00;00"); + expect(Timecode(1078920 * 2, 59.94, true).toString()).to.be("10:00:00;00"); + expect(Timecode(3597 * 2 + 1, 59.94, true).toString()).to.be("00:01:59;59"); + }); + it("non-drop-frame counts", function () { + expect(Timecode("00:10:00:00", 25).frameCount).to.be(15000); + expect(Timecode("10:00:00:00", 25).frameCount).to.be(900000); + expect(Timecode("00:02:00:00", 25).frameCount).to.be(3000); + expect(Timecode("00:01:59:24", 25).frameCount).to.be(2999); + expect(Timecode(15000, 25).toString()).to.be("00:10:00:00"); + expect(Timecode(900000, 25).toString()).to.be("10:00:00:00"); + expect(Timecode(2999, 25).toString()).to.be("00:01:59:24"); + }); + + it("non-standard frame rates", function () { + expect(Timecode("00:10:00:00", 28).frameCount).to.be(16800); + }); + + it("parses semicolon-delimited timecodes", () => { + const tc = Timecode("00;10;00;23", 29.97); + expect(tc.frameCount).to.be(18005); + expect(tc.dropFrame).to.be(true); + expect(tc.toString()).to.be("00:10:00;23"); + }); }); -describe('String conversions', function(){ - it ('back and forth works',function(){ - expect(Timecode('12:34:56;23',29.97).toString()).to.be('12:34:56;23'); - expect(Timecode('01:02:03;04',29.97).toString()).to.be('01:02:03;04'); - expect(Timecode('12:34:56;57',59.94).toString()).to.be('12:34:56;57'); - expect(Timecode('01:02:03;04',59.94).toString()).to.be('01:02:03;04'); - }); - it ('implicit calls to toString()',function(){ - expect('+'.concat(Timecode('12:34:56;23',29.97),'+')).to.be('+12:34:56;23+'); - expect(/12.34.56.23/.test(Timecode('12:34:56;23',29.97))); - }); - it ('toString(\'field\')',function(){ - expect(Timecode('12:34:56;23',29.97).toString('field')).to.be('12:34:56;23.0'); - expect(Timecode('01:02:03;04',29.97).toString('field')).to.be('01:02:03;04.0'); - expect(Timecode('12:34:56;57',59.94).toString('field')).to.be('12:34:56;28.1'); - expect(Timecode('01:02:03;04',59.94).toString('field')).to.be('01:02:03;02.0'); - }); - it ('toString(\'unknown-format\')',function(){ - expect(function() {Timecode('12:34:56;23',29.97).toString('unknown-format')}).to.throwException(); - }); +describe("String conversions", function () { + it("back and forth works", function () { + expect(Timecode("12:34:56;23", 29.97).toString()).to.be("12:34:56;23"); + expect(Timecode("01:02:03;04", 29.97).toString()).to.be("01:02:03;04"); + expect(Timecode("12:34:56;57", 59.94).toString()).to.be("12:34:56;57"); + expect(Timecode("01:02:03;04", 59.94).toString()).to.be("01:02:03;04"); + }); + it("implicit calls to toString()", function () { + expect("+".concat(Timecode("12:34:56;23", 29.97), "+")).to.be( + "+12:34:56;23+" + ); + expect(/12.34.56.23/.test(Timecode("12:34:56;23", 29.97))); + }); + it("toString('field')", function () { + expect(Timecode("12:34:56;23", 29.97).toString("field")).to.be( + "12:34:56;23.0" + ); + expect(Timecode("01:02:03;04", 29.97).toString("field")).to.be( + "01:02:03;04.0" + ); + expect(Timecode("12:34:56;57", 59.94).toString("field")).to.be( + "12:34:56;28.1" + ); + expect(Timecode("01:02:03;04", 59.94).toString("field")).to.be( + "01:02:03;02.0" + ); + }); + it("toString('unknown-format')", function () { + expect(function () { + Timecode("12:34:56;23", 29.97).toString("unknown-format"); + }).to.throwException(); + }); }); -describe('Timecode arithmetic', function(){ - it ('Timecode() as primitive', function() { - var t = Timecode('01:23:45;06',29.97); - expect(t.frameCount).to.be(150606); - expect(t+1).to.be(150607); - expect(12*t).to.be(150606*12); - expect(-t).to.be(-150606); - expect(Math.round(t)).to.be(150606); - t++; - expect(t).to.be(150607); - expect(t).to.be.a('number'); // t is not a timecode anymore! - }); - it ('Timecode().add() and .subtract()', function() { - var t = Timecode('01:23:45;06',29.97); - expect(t.add(60).toString()).to.be('01:23:47;06') - expect(function(){Timecode('00:00:10;00',29.97).add(-301)}).to.throwError(); // below zero - expect(Timecode('23:59:40;00',29.97).add(Timecode('00:00:21;00',29.97)).toString()).to.be('00:00:01;00'); // wraparound - - t = Timecode('01:23:45;06',29.97); - expect(t.subtract(60).toString()).to.be('01:23:43;06') - expect(function(){Timecode('00:00:10;00',29.97).subtract(301)}).to.throwError(); // below zero - - expect(Timecode('01:23:45;06',29.97).add('01:23:13;01').toString()).to.be('02:46:58;07'); - - // Covering the error with _frameCountToTimeCode() altering this.frameCount - t = Timecode('00:01:15;00',29.97); - var t2 = Timecode('00:01:15;00',29.97); - t2.add(0); - expect(t.frameCount).to.be(t2.frameCount); - t2.add(12345); - expect(t.frameCount).to.be(t2.frameCount-12345); - }); - it ('handles rollover to new day when permitted', function() { - expect(function() { new Timecode(0,29.97).subtract(new Timecode('23:00:01;00',29.97)); }).to.throwError(); - expect(new Timecode(0,29.97).subtract(new Timecode('23:30:00;00',29.97), 1).toString()).to.be('00:30:00;00'); - expect(function() { new Timecode(0,29.97).subtract(new Timecode('22:30:00;00',29.97), 1); }).to.throwError(); - expect(new Timecode('01:00:00;00',29.97).subtract(new Timecode('23:30:00;00',29.97), 2).toString()).to.be('01:30:00;00'); - }); - it('Ensures source frame rate is kept when adding two Timecode objects', function() { - expect(new Timecode('00:00:00:00', 25, false).add('00:01:00:00').frameCount).to.be(1500); - }); +describe("Timecode arithmetic", function () { + it("Timecode() as primitive", function () { + var t = Timecode("01:23:45;06", 29.97); + expect(t.frameCount).to.be(150606); + expect(t + 1).to.be(150607); + expect(12 * t).to.be(150606 * 12); + expect(-t).to.be(-150606); + expect(Math.round(t)).to.be(150606); + t++; + expect(t).to.be(150607); + expect(t).to.be.a("number"); // t is not a timecode anymore! + }); + it("Timecode().add() and .subtract()", function () { + var t = Timecode("01:23:45;06", 29.97); + expect(t.add(60).toString()).to.be("01:23:47;06"); + expect(function () { + Timecode("00:00:10;00", 29.97).add(-301); + }).to.throwError(); // below zero + expect( + Timecode("23:59:40;00", 29.97) + .add(Timecode("00:00:21;00", 29.97)) + .toString() + ).to.be("00:00:01;00"); // wraparound + + t = Timecode("01:23:45;06", 29.97); + expect(t.subtract(60).toString()).to.be("01:23:43;06"); + expect(function () { + Timecode("00:00:10;00", 29.97).subtract(301); + }).to.throwError(); // below zero + + expect(Timecode("01:23:45;06", 29.97).add("01:23:13;01").toString()).to.be( + "02:46:58;07" + ); + + // Covering the error with _frameCountToTimeCode() altering this.frameCount + t = Timecode("00:01:15;00", 29.97); + var t2 = Timecode("00:01:15;00", 29.97); + t2.add(0); + expect(t.frameCount).to.be(t2.frameCount); + t2.add(12345); + expect(t.frameCount).to.be(t2.frameCount - 12345); + }); + it("handles rollover to new day when permitted", function () { + expect(function () { + new Timecode(0, 29.97).subtract(new Timecode("23:00:01;00", 29.97)); + }).to.throwError(); + expect( + new Timecode(0, 29.97) + .subtract(new Timecode("23:30:00;00", 29.97), 1) + .toString() + ).to.be("00:30:00;00"); + expect(function () { + new Timecode(0, 29.97).subtract(new Timecode("22:30:00;00", 29.97), 1); + }).to.throwError(); + expect( + new Timecode("01:00:00;00", 29.97) + .subtract(new Timecode("23:30:00;00", 29.97), 2) + .toString() + ).to.be("01:30:00;00"); + }); + it("Ensures source frame rate is kept when adding two Timecode objects", function () { + expect( + new Timecode("00:00:00:00", 25, false).add("00:01:00:00").frameCount + ).to.be(1500); + }); }); -describe('Date() operations', function(){ - it ('Date() initializers work', function(){ - var t = new Timecode( new Date(0,0,0,1,2,13,200), 29.97, true ); - expect( t.frameCount ).to.be(111884); - expect( t.toString()).to.be('01:02:13;06'); - - var t2 = new Timecode( new Date(0,0,0,10,40,15,520), 25, false ); - expect( t2.frameCount ).to.be(960388); - expect( t2.toString()).to.be('10:40:15:13'); - }); - it ('Timecode to Date()', function(){ - var d = Timecode('01:23:45;10',29.97).toDate(); - expect( d.getHours()).to.be(1); - expect( d.getMinutes()).to.be(23); - expect( d.getSeconds()).to.be(45); - expect( d.getMilliseconds()).to.be(358); - }); +describe("Date() operations", function () { + it("Date() initializers work", function () { + var t = new Timecode(new Date(0, 0, 0, 1, 2, 13, 200), 29.97, true); + expect(t.frameCount).to.be(111884); + expect(t.toString()).to.be("01:02:13;06"); + + var t2 = new Timecode(new Date(0, 0, 0, 10, 40, 15, 520), 25, false); + expect(t2.frameCount).to.be(960388); + expect(t2.toString()).to.be("10:40:15:13"); + }); + it("Timecode to Date()", function () { + var d = Timecode("01:23:45;10", 29.97).toDate(); + expect(d.getHours()).to.be(1); + expect(d.getMinutes()).to.be(23); + expect(d.getSeconds()).to.be(45); + expect(d.getMilliseconds()).to.be(358); + }); }); -describe('DST handling', function() { - var clock; - - function clearDate(d) { - d.setYear(0); - d.setMonth(0); - d.setDate(1); - } - - function checkDst(d) { - // we need to fake out 'new Date()', since this issue only happens day of. - clock = sinon.useFakeTimers(d); - - var t = new Timecode(d, 29.97, true); - var o = t.toDate(); - // console.log(d.toString(), '->', o.toString()); - clearDate(d); - clearDate(o); - expect(o.toString()).to.be(d.toString()); - } - - afterEach(function() { - clock.restore(); - }); - - it ('handles DST start 1am', function() { - checkDst(new Date(2018,2,11,1,0,0,200)); - checkDst(new Date(2018,2,11,1,59,59,200)); - }); - - it ('handles DST start 2am', function() { - checkDst(new Date(2018,2,11,2,0,0,200)); - checkDst(new Date(2018,2,11,2,59,59,200)); - checkDst(new Date(2018,2,11,3,0,0,200)); - }); - - it ('handles DST end 1am', function() { - checkDst(new Date(2018,10,4,1,0,0,200)); - checkDst(new Date(2018,10,4,1,59,59,200)); - }); - - it ('handles DST end 2am', function() { - checkDst(new Date(2018,10,4,2,0,0,200)); - checkDst(new Date(2018,10,4,2,59,59,200)); - checkDst(new Date(2018,10,4,3,0,0,200)); - }); +describe("DST handling", function () { + var clock; + + function clearDate(d) { + d.setYear(0); + d.setMonth(0); + d.setDate(1); + } + + function checkDst(d) { + // we need to fake out 'new Date()', since this issue only happens day of. + clock = sinon.useFakeTimers(d); + + var t = new Timecode(d, 29.97, true); + var o = t.toDate(); + // console.log(d.toString(), '->', o.toString()); + clearDate(d); + clearDate(o); + expect(o.toString()).to.be(d.toString()); + } + + afterEach(function () { + clock.restore(); + }); + + it("handles DST start 1am", function () { + checkDst(new Date(2018, 2, 11, 1, 0, 0, 200)); + checkDst(new Date(2018, 2, 11, 1, 59, 59, 200)); + }); + + it("handles DST start 2am", function () { + checkDst(new Date(2018, 2, 11, 2, 0, 0, 200)); + checkDst(new Date(2018, 2, 11, 2, 59, 59, 200)); + checkDst(new Date(2018, 2, 11, 3, 0, 0, 200)); + }); + + it("handles DST end 1am", function () { + checkDst(new Date(2018, 10, 4, 1, 0, 0, 200)); + checkDst(new Date(2018, 10, 4, 1, 59, 59, 200)); + }); + it("handles DST end 2am", function () { + checkDst(new Date(2018, 10, 4, 2, 0, 0, 200)); + checkDst(new Date(2018, 10, 4, 2, 59, 59, 200)); + checkDst(new Date(2018, 10, 4, 3, 0, 0, 200)); + }); });