├── .clasp.json ├── .claspignore ├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── Code.js ├── Code.test.js ├── LICENSE ├── README.md ├── appsscript.json ├── package-lock.json ├── package.json └── preview.png /.clasp.json: -------------------------------------------------------------------------------- 1 | {"scriptId":"1tJlMAetSZUEbq7smB7reF4PLCrYVpQapjeP7efK-TzlCs8oYodb5SBi6"} 2 | -------------------------------------------------------------------------------- /.claspignore: -------------------------------------------------------------------------------- 1 | # Ignore all files... 2 | **/** 3 | 4 | # Except Code.js and appsscript.json. 5 | !Code.js 6 | !appsscript.json 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | -------------------------------------------------------------------------------- /Code.js: -------------------------------------------------------------------------------- 1 | // gsheets-timecode: Google Sheets Apps Script custom functions for working 2 | // with video timecode standards and wall time durations. 3 | // 4 | // Designed for film & television composers, though these may be useful for 5 | // anyone who works with timecode values in Google Sheets. 6 | // 7 | // Author: Eric Barndollar (https://barndollarmusic.com) 8 | // 9 | // The easiest way to use yourself is to use File > Make a copy on this Sheet: 10 | // https://docs.google.com/spreadsheets/d/1xPi0lxi4-4NmZmNoTXXoCNa0FGIAhwi2QCPjTABJCw4/edit?usp=sharing 11 | // 12 | // Code on GitHub: https://github.com/barndollarmusic/gsheets-timecode 13 | // (If you made a copy, check above link for the most updated version). 14 | // 15 | // This is open source software that is free to use and share, as covered by the 16 | // MIT License. 17 | // 18 | // Custom Functions list (with example arguments): 19 | // - TC_TO_WALL_SECS("00:00:01:02", "50.00", "non-drop"): 1.04 secs (wall time) 20 | // - WALL_SECS_BETWEEN_TCS("00:00:01:03", "00:02:05:11", "24.00", "non-drop"): 21 | // 124.33333333... secs (wall time) 22 | // - WALL_SECS_TO_DURSTR(3765): "1h 02m 45s" (human-readable duration string) 23 | // - WALL_SECS_TO_TC_LEFT(1.041, "50.00", "non-drop"): "00:00:01:02" (timecode <= wallSecs) 24 | // - WALL_SECS_TO_TC_RIGHT(1.041, "50.00", "non-drop"): "00:00:01:03" (timecode >= wallSecs) 25 | // - TC_ERROR("01:02:03:04", "23.976", "non-drop"): error string if invalid 26 | // 27 | // - TC_TO_FRAMEIDX("00:00:01:02", "50.00", "non-drop"): 52 (frame index) 28 | // - FRAMEIDX_TO_TC(52, "50.00", "non-drop"): "00:00:01:02" (timecode) 29 | // - FRAMEIDX_TO_WALL_SECS(52, "50.00", "non-drop"): 1.04 secs (wall time) 30 | // - WALL_SECS_TO_FRAMEIDX_LEFT(1.041, "50.00", "non-drop"): 52 (frame index <= time) 31 | // - WALL_SECS_TO_FRAMEIDX_RIGHT(1.041, "50.00", "non-drop"): 53 (frame index >= time) 32 | 33 | //================================================================================================== 34 | // Platform-Specific 35 | //================================================================================================== 36 | 37 | // NOTE: Any code specific to Google Sheets or Microsoft Excel should go here. 38 | 39 | /** 40 | * @param {string} msg 41 | * @return {Error} 42 | * @private 43 | */ 44 | function inputValueErr_(msg) { 45 | // NOTE: Microsoft Excel requires a specific Error type, while Google Sheets 46 | // just displays the error message from any Error object. 47 | return Error(msg); 48 | } 49 | 50 | //================================================================================================== 51 | // Common Code 52 | //================================================================================================== 53 | 54 | // NOTE: All remaining code should be exactly the same across Google Sheets and Microsoft Excel. 55 | 56 | /** 57 | * Support these values of input frameRate strings. Require exactly 2 or 3 58 | * decimal digits of precision to avoid confusion as to e.g. whether "24" 59 | * means 24.000 or 23.976. 60 | * @private 61 | */ 62 | const FRAME_RATES_ = { 63 | '23.976': {frames: 24000, perWallSecs: 1001}, // 23.976023976023976... 64 | '23.98': {frames: 24000, perWallSecs: 1001}, // 23.976023976023976... 65 | '24.000': {frames: 24, perWallSecs: 1}, 66 | '24.00': {frames: 24, perWallSecs: 1}, 67 | '25.000': {frames: 25, perWallSecs: 1}, 68 | '25.00': {frames: 25, perWallSecs: 1}, 69 | '29.970': {frames: 30000, perWallSecs: 1001}, // 29.97002997002997... 70 | '29.97': {frames: 30000, perWallSecs: 1001}, // 29.97002997002997... 71 | '30.000': {frames: 30, perWallSecs: 1}, 72 | '30.00': {frames: 30, perWallSecs: 1}, 73 | '47.952': {frames: 48000, perWallSecs: 1001}, // 47.952047952047952... 74 | '47.95': {frames: 48000, perWallSecs: 1001}, // 47.952047952047952... 75 | '48.000': {frames: 48, perWallSecs: 1}, 76 | '48.00': {frames: 48, perWallSecs: 1}, 77 | '50.000': {frames: 50, perWallSecs: 1}, 78 | '50.00': {frames: 50, perWallSecs: 1}, 79 | '59.940': {frames: 60000, perWallSecs: 1001}, // 59.94005994005994... 80 | '59.94': {frames: 60000, perWallSecs: 1001}, // 59.94005994005994... 81 | '60.000': {frames: 60, perWallSecs: 1}, 82 | '60.00': {frames: 60, perWallSecs: 1}, 83 | }; 84 | 85 | /** 86 | * The number of frames dropped per 10 minutes for supported drop frame 87 | * rate timecode standards. 88 | * @private 89 | */ 90 | const DROP_FRAMES_PER_10MINS_ = { 91 | '29.970': 18, // First 2 frames of minutes x1, x2, ..., x9. 92 | '29.97': 18, // First 2 frames of minutes x1, x2, ..., x9. 93 | '59.940': 36, // First 4 frames of minutes x1, x2, ..., x9. 94 | '59.94': 36, // First 4 frames of minutes x1, x2, ..., x9. 95 | }; 96 | 97 | /** @private */ 98 | const FRAME_RATE_STR_FMT_ = /^[0-9][0-9].[0-9][0-9][0-9]?$/; 99 | 100 | /** 101 | * Internal configuration data for a timecode standard. 102 | * @typedef {{ 103 | * frames: number, 104 | * perWallSecs: number, 105 | * intFps: number, 106 | * dropFramesPer10Mins: number, 107 | * }} TimecodeStandard 108 | */ 109 | 110 | /** 111 | * @param {string} frameRateStr 112 | * @param {string} dropTypeStr 113 | * @return {TimecodeStandard} 114 | * @private 115 | */ 116 | function parseTcStd_(frameRateStr, dropTypeStr) { 117 | if (typeof frameRateStr !== 'string') { 118 | throw inputValueErr_('frameRate must be a single plain text value'); 119 | } 120 | frameRateStr = frameRateStr.trim(); 121 | 122 | if (!FRAME_RATE_STR_FMT_.test(frameRateStr)) { 123 | throw inputValueErr_( 124 | 'frameRate must contain 2 or 3 digits after period (e.g. "23.976" or "24.00")'); 125 | } 126 | const frameRate = FRAME_RATES_[frameRateStr]; 127 | if (!frameRate) { 128 | throw inputValueErr_(`Unsupported frame rate: "${frameRateStr}"`); 129 | } 130 | 131 | if (typeof dropTypeStr !== 'string') { 132 | throw inputValueErr_('dropType must be a single plain text value'); 133 | } 134 | dropTypeStr = dropTypeStr.trim().toLowerCase(); 135 | if ((dropTypeStr !== 'drop') && (dropTypeStr !== 'non-drop')) { 136 | throw inputValueErr_('dropType value must be "non-drop" or "drop" (without quotes)'); 137 | } 138 | 139 | let dropFramesPer10Mins = 0; 140 | if (dropTypeStr === 'drop') { 141 | if (!(frameRateStr in DROP_FRAMES_PER_10MINS_)) { 142 | throw inputValueErr_(`frameRate ${frameRateStr} must be non-drop`); 143 | } 144 | dropFramesPer10Mins = DROP_FRAMES_PER_10MINS_[frameRateStr]; 145 | } 146 | 147 | return { 148 | frames: frameRate.frames, 149 | perWallSecs: frameRate.perWallSecs, 150 | intFps: Math.ceil(frameRate.frames / frameRate.perWallSecs), 151 | dropFramesPer10Mins: dropFramesPer10Mins, 152 | }; 153 | } 154 | 155 | /** 156 | * Parsed numerical timecode. 157 | * @typedef {{hh: number, mm: number, ss: number, ff: number}} ParsedTimecode 158 | */ 159 | 160 | /** 161 | * @param {string|number} timecode 162 | * @return {ParsedTimecode} 163 | * @private 164 | */ 165 | function parseTc_(timecode) { 166 | // If in number format, must be a positive integer in range [0, 99999999]. 167 | if (typeof timecode === 'number') { 168 | if (!Number.isInteger(timecode) || (timecode < 0) || (99999999 < timecode)) { 169 | throw inputValueErr_('numerical timecode must be an integer in [0, 99999999] range'); 170 | } 171 | 172 | let digits = timecode; 173 | const ff = digits % 100; 174 | 175 | digits = (digits - ff) / 100; 176 | const ss = digits % 100; 177 | 178 | digits = (digits - ss) / 100; 179 | const mm = digits % 100; 180 | 181 | digits = (digits - mm) / 100; 182 | const hh = digits % 100; 183 | 184 | return { 185 | hh: hh, 186 | mm: mm, 187 | ss: ss, 188 | ff: ff, 189 | }; 190 | } 191 | 192 | if (typeof timecode !== 'string') { 193 | throw inputValueErr_('timecode must be a single plain text value or custom format number'); 194 | } 195 | 196 | const matches = timecode.trim().match(TC_STR_FMT_); 197 | if (!matches) { 198 | throw inputValueErr_(`timecode must be in HH:MM:SS:FF format: "${timecode}"`); 199 | } 200 | 201 | return { 202 | hh: Number(matches[1]), 203 | mm: Number(matches[2]), 204 | ss: Number(matches[3]), 205 | ff: Number(matches[4]), 206 | }; 207 | } 208 | 209 | /** 210 | * @param {ParsedTimecode} tc 211 | * @param {TimecodeStandard} tcStd 212 | * @return {boolean} 213 | * @private 214 | */ 215 | function isDropSec_(tc, tcStd) { 216 | if (tcStd.dropFramesPer10Mins === 0) { 217 | return false; // Not a drop frame standard. 218 | } 219 | 220 | // A block of frames is dropped from the first second (SS=00) of each minute 221 | // that is not divisible by 10 (MM=x1, x2, ..., x9). 222 | return (tc.ss === 0) && ((tc.mm % 10) !== 0); 223 | } 224 | 225 | /** 226 | * @param {TimecodeStandard} tcStd 227 | * @return {number} 228 | * @private 229 | */ 230 | function framesPerDroppedBlock_(tcStd) { 231 | // A block of frames is dropped from the first second (SS=00) of 9 out of every 232 | // 10 minutes. For 29.97 fps, for example, there are 18 frames dropped per 10 233 | // minutes, in blocks of 2 frames at a time. 234 | return tcStd.dropFramesPer10Mins / 9; 235 | } 236 | 237 | /** @private */ 238 | const TC_STR_FMT_ = /^([0-9][0-9])[:;]([0-9][0-9])[:;]([0-9][0-9])[:;]([0-9][0-9])$/; 239 | 240 | /** @private */ 241 | const MINS_PER_HR_ = 60; 242 | 243 | /** @private */ 244 | const SECS_PER_MIN_ = 60; 245 | 246 | /** 247 | * @param {string|number} timecode 248 | * @param {ParsedTimecode} tc 249 | * @param {TimecodeStandard} tcStd 250 | * @throws {Error} if invalid. 251 | * @private 252 | */ 253 | function validateTc_(timecode, tc, tcStd) { 254 | // Ensure each segment of timecode is in valid range (and not a dropped frame). 255 | 256 | // All digit HH values (00-99) are valid... 257 | 258 | if (tc.mm >= MINS_PER_HR_) { 259 | throw inputValueErr_(`timecode MM must be in range 00-59: "${tc.mm}"`); 260 | } 261 | 262 | if (tc.ss >= SECS_PER_MIN_) { 263 | throw inputValueErr_(`timecode SS must be in range 00-59: "${tc.ss}"`); 264 | } 265 | 266 | if (tc.ff >= tcStd.intFps) { 267 | throw inputValueErr_(`timecode FF must be in range 00-${tcStd.intFps - 1}: "${tc.ff}"`); 268 | } 269 | 270 | // Frame number must not be a dropped frame. 271 | if (isDropSec_(tc, tcStd)) { 272 | if (tc.ff < framesPerDroppedBlock_(tcStd)) { 273 | throw inputValueErr_(`timecode invalid: "${tcToStr_(tc)}" is a dropped frame number`); 274 | } 275 | } 276 | 277 | // If string-format timecode used semicolons, make sure it was a drop standard. 278 | if (typeof timecode === 'string') { 279 | const hasSemicolons = (timecode.indexOf(';') >= 0); 280 | if (hasSemicolons && (tcStd.dropFramesPer10Mins === 0)) { 281 | throw inputValueErr_(`only drop timecode may use semi-colon separator: "${timecode}"`); 282 | } 283 | } 284 | } 285 | 286 | /** 287 | * Returns empty string if the input timecode value is valid time in the given timecode 288 | * standard, or a non-empty error otherwise. 289 | * @param {string|number} timecode Timecode value in "HH:MM:SS:FF" format (without 290 | * quotes), or an integer number (e.g. 4332211 will be interpreted as 04:33:22:11). 291 | * May use semicolons in drop frame standards. 292 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 293 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 294 | * @param {string} dropType "drop" or "non-drop". 295 | * @return {string} Empty string if valid, or non-empty error message. 296 | * @customFunction 297 | */ 298 | function TC_ERROR(timecode, frameRate, dropType) { 299 | try { 300 | const tcStd = parseTcStd_(frameRate, dropType); 301 | const tc = parseTc_(timecode); 302 | validateTc_(timecode, tc, tcStd); 303 | return ''; 304 | } catch (e) { 305 | return e.toString(); 306 | } 307 | } 308 | 309 | /** 310 | * Converts input timecode to frame index (where 00:00:00:00 has index 0, 311 | * 00:00:00:01 index 1, etc.). 312 | * 313 | * If this is a drop frame standard, dropped frames are not given indexes 314 | * (so in 29.97 drop, 00:00:59:29 has index 1799 and 00:01:00:02 has index 1800). 315 | * @param {string|number} timecode Timecode value in "HH:MM:SS:FF" format (without 316 | * quotes), or an integer number (e.g. 4332211 will be interpreted as 04:33:22:11). 317 | * May use semicolons in drop frame standards. 318 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 319 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 320 | * @param {string} dropType "drop" or "non-drop". 321 | * @return {number} Frame index. 322 | * @customFunction 323 | */ 324 | function TC_TO_FRAMEIDX(timecode, frameRate, dropType) { 325 | const tcStd = parseTcStd_(frameRate, dropType); 326 | const tc = parseTc_(timecode); 327 | validateTc_(timecode, tc, tcStd); 328 | return tcToFrameIdx_(tc, tcStd); 329 | } 330 | 331 | /** 332 | * @param {ParsedTimecode} tc 333 | * @param {TimecodeStandard} tcStd 334 | * @return {number} 335 | * @private 336 | */ 337 | function tcToFrameIdx_(tc, tcStd) { 338 | // Calculate first ignoring dropped frames. 339 | const tcTotalMins = (MINS_PER_HR_ * tc.hh) + tc.mm; 340 | const tcTotalSecs = (SECS_PER_MIN_ * tcTotalMins) + tc.ss; 341 | let frameIdx = (tcStd.intFps * tcTotalSecs) + tc.ff; 342 | 343 | // Adjust for any frame numbers that were dropped. 344 | if (tcStd.dropFramesPer10Mins > 0) { 345 | // Frames dropped through start of HH: 346 | const framesDroppedPerHr = 6 * tcStd.dropFramesPer10Mins; 347 | frameIdx -= tc.hh * framesDroppedPerHr; 348 | 349 | // Frames dropped from start of HH to start of this 10 minute block: 350 | frameIdx -= Math.floor(tc.mm / 10) * tcStd.dropFramesPer10Mins; 351 | 352 | // Frames dropped since start of this 10 minute block: 353 | frameIdx -= (tc.mm % 10) * framesPerDroppedBlock_(tcStd); 354 | } 355 | 356 | return frameIdx; 357 | } 358 | 359 | /** 360 | * Converts input frame index to wall time in seconds offset from origin 361 | * time 00:00:00:00. 362 | * @param {number} frameIdx The 0-based frame index. 363 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 364 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 365 | * @param {string} dropType "drop" or "non-drop". 366 | * @return {number} Wall time in seconds (possibly fractional). 367 | * @customFunction 368 | */ 369 | function FRAMEIDX_TO_WALL_SECS(frameIdx, frameRate, dropType) { 370 | const tcStd = parseTcStd_(frameRate, dropType); 371 | if (!Number.isInteger(frameIdx) || (frameIdx < 0)) { 372 | throw inputValueErr_('frameIdx must be non-negative integer'); 373 | } 374 | return frameIdxToWallSecs_(frameIdx, tcStd); 375 | } 376 | 377 | /** 378 | * @param {number} frameIdx 379 | * @param {TimecodeStandard} tcStd 380 | * @return {number} 381 | * @private 382 | */ 383 | function frameIdxToWallSecs_(frameIdx, tcStd) { 384 | return frameIdx * tcStd.perWallSecs / tcStd.frames; 385 | } 386 | 387 | /** 388 | * Converts input timecode to wall time in seconds offset from origin 389 | * time 00:00:00:00. 390 | * @param {string|number} timecode Timecode value in "HH:MM:SS:FF" format (without 391 | * quotes), or an integer number (e.g. 4332211 will be interpreted as 04:33:22:11). 392 | * May use semicolons in drop frame standards. 393 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 394 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 395 | * @param {string} dropType "drop" or "non-drop". 396 | * @return {number} Wall time in seconds (possibly fractional). 397 | * @customFunction 398 | */ 399 | function TC_TO_WALL_SECS(timecode, frameRate, dropType) { 400 | const tcStd = parseTcStd_(frameRate, dropType); 401 | const tc = parseTc_(timecode); 402 | validateTc_(timecode, tc, tcStd); 403 | 404 | const frameIdx = tcToFrameIdx_(tc, tcStd); 405 | return frameIdxToWallSecs_(frameIdx, tcStd); 406 | } 407 | 408 | /** 409 | * Returns wall time in seconds between the given start and end timecodes. If end 410 | * is before start, the returned value will be negative. 411 | * @param {string|number} start Start timecode value in "HH:MM:SS:FF" format (without 412 | * quotes), or an integer number (e.g. 4332211 will be interpreted as 04:33:22:11). 413 | * May use semicolons in drop frame standards. 414 | * @param {string|number} end End timecode value in "HH:MM:SS:FF" format (without 415 | * quotes), or an integer number (e.g. 4332211 will be interpreted as 04:33:22:11). 416 | * May use semicolons in drop frame standards. 417 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 418 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 419 | * @param {string} dropType "drop" or "non-drop". 420 | * @return {number} Duration from start to end as measured by wall time in seconds 421 | * (possibly fractional). 422 | * @customFunction 423 | */ 424 | function WALL_SECS_BETWEEN_TCS(start, end, frameRate, dropType) { 425 | const tcStd = parseTcStd_(frameRate, dropType); 426 | 427 | const startTc = parseTc_(start); 428 | validateTc_(start, startTc, tcStd); 429 | 430 | const endTc = parseTc_(end); 431 | validateTc_(end, endTc, tcStd); 432 | 433 | const startIdx = tcToFrameIdx_(startTc, tcStd); 434 | const endIdx = tcToFrameIdx_(endTc, tcStd); 435 | 436 | return (endIdx - startIdx) * tcStd.perWallSecs / tcStd.frames; 437 | } 438 | 439 | /** 440 | * Converts time in wall seconds to a more human-readable duration string. Rounds 441 | * fractional seconds to the nearest value (with 0.5 rounding up). 442 | * 443 | * Example output for 4994.5 seconds is "1h 23m 15s". 444 | * @param {number} wallSecs Duration in wall seconds (possibly fractional). 445 | * @return {string} Human-readable duration string. 446 | * @customFunction 447 | */ 448 | function WALL_SECS_TO_DURSTR(wallSecs) { 449 | if ((typeof wallSecs !== 'number') || !Number.isFinite(wallSecs)) { 450 | throw inputValueErr_('wallSecs must be a finite number'); 451 | } 452 | 453 | let isNegative = false; 454 | if (wallSecs < 0) { 455 | wallSecs *= -1; 456 | isNegative = true; 457 | } 458 | 459 | wallSecs = Math.round(wallSecs); 460 | 461 | let output = ''; 462 | if (isNegative && (wallSecs !== 0)) { 463 | output += '(-) '; 464 | } 465 | 466 | const hh = Math.floor(wallSecs / (MINS_PER_HR_ * SECS_PER_MIN_)); 467 | wallSecs -= (MINS_PER_HR_ * SECS_PER_MIN_) * hh; 468 | 469 | const mm = Math.floor(wallSecs / SECS_PER_MIN_); 470 | wallSecs -= SECS_PER_MIN_ * mm; 471 | 472 | const ss = wallSecs; 473 | 474 | // Output hh only if non-zero. No zero padding. 475 | if (hh > 0) { 476 | output += hh; 477 | output += 'h '; 478 | } 479 | 480 | // Output mm only if non-zero. Zero pad if needed if there are hours. 481 | if ((hh > 0) || (mm > 0)) { 482 | output += (hh > 0) ? String(mm).padStart(2, '0') : mm; 483 | output += 'm '; 484 | } 485 | 486 | // Always output ss. Zero pad if needed for 2 digits. 487 | output += String(ss).padStart(2, '0'); 488 | output += 's'; 489 | 490 | return output; 491 | } 492 | 493 | /** 494 | * Returns frame index of closest frame before or exactly equal to the given 495 | * wallSecs (offset from origin 00:00:00:00). 496 | * 497 | * Note that negative wallSecs will yield negative frame indexes. 498 | * @param {number} wallSecs Time in wall seconds (possibly fractional) offset from 499 | * origin 00:00:00:00. 500 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 501 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 502 | * @param {string} dropType "drop" or "non-drop". 503 | * @return {number} Integer frame index <= given wallSecs. 504 | * @customFunction 505 | */ 506 | function WALL_SECS_TO_FRAMEIDX_LEFT(wallSecs, frameRate, dropType) { 507 | const tcStd = parseTcStd_(frameRate, dropType); 508 | 509 | const fractionalFrameIdx = wallSecsToFractionalFrameIdx_(wallSecs, tcStd); 510 | return Math.floor(fractionalFrameIdx); 511 | } 512 | 513 | /** 514 | * Returns frame index of closest frame after or exactly equal to the given 515 | * wallSecs (offset from origin 00:00:00:00). 516 | * 517 | * Note that negative wallSecs will yield negative frame indexes. 518 | * @param {number} wallSecs Time in wall seconds (possibly fractional) offset from 519 | * origin 00:00:00:00. 520 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 521 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 522 | * @param {string} dropType "drop" or "non-drop". 523 | * @return {number} Integer frame index >= given wallSecs. 524 | * @customFunction 525 | */ 526 | function WALL_SECS_TO_FRAMEIDX_RIGHT(wallSecs, frameRate, dropType) { 527 | const tcStd = parseTcStd_(frameRate, dropType); 528 | 529 | const fractionalFrameIdx = wallSecsToFractionalFrameIdx_(wallSecs, tcStd); 530 | return Math.ceil(fractionalFrameIdx); 531 | } 532 | 533 | /** 534 | * @param {number} wallSecs 535 | * @param {TimecodeStandard} tcStd 536 | * @return {number} 537 | * @private 538 | */ 539 | function wallSecsToFractionalFrameIdx_(wallSecs, tcStd) { 540 | if (!Number.isFinite(wallSecs)) { 541 | throw inputValueErr_('wallSecs must be a finite number: ' + wallSecs); 542 | } 543 | 544 | return wallSecs * tcStd.frames / tcStd.perWallSecs; 545 | } 546 | 547 | /** 548 | * Returns timecode string of closest frame before or exactly equal to the given 549 | * wallSecs (offset from origin 00:00:00:00). 550 | * 551 | * Note that negative wallSecs are NOT supported. 552 | * @param {number} wallSecs Time in wall seconds (possibly fractional) offset from 553 | * origin 00:00:00:00. 554 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 555 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 556 | * @param {string} dropType "drop" or "non-drop". 557 | * @return {string} Timecode of nearest frame <= wallSecs. 558 | * @customFunction 559 | */ 560 | function WALL_SECS_TO_TC_LEFT(wallSecs, frameRate, dropType) { 561 | const tcStd = parseTcStd_(frameRate, dropType); 562 | 563 | const fractionalFrameIdx = wallSecsToFractionalFrameIdx_(wallSecs, tcStd); 564 | const frameIdx = Math.floor(fractionalFrameIdx); 565 | return frameIdxToTc_(frameIdx, tcStd); 566 | } 567 | 568 | /** 569 | * Returns timecode string of closest frame after or exactly equal to the given 570 | * wallSecs (offset from origin 00:00:00:00). 571 | * 572 | * Note that negative wallSecs are NOT supported. 573 | * @param {number} wallSecs Time in wall seconds (possibly fractional) offset from 574 | * origin 00:00:00:00. 575 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 576 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 577 | * @param {string} dropType "drop" or "non-drop". 578 | * @return {string} Timecode of nearest frame >= wallSecs. 579 | * @customFunction 580 | */ 581 | function WALL_SECS_TO_TC_RIGHT(wallSecs, frameRate, dropType) { 582 | const tcStd = parseTcStd_(frameRate, dropType); 583 | 584 | const fractionalFrameIdx = wallSecsToFractionalFrameIdx_(wallSecs, tcStd); 585 | const frameIdx = Math.ceil(fractionalFrameIdx); 586 | return frameIdxToTc_(frameIdx, tcStd); 587 | } 588 | 589 | /** 590 | * Returns timecode string for given frame index. 591 | * 592 | * Note that negative frameIdx values are NOT supported. 593 | * @param {number} frameIdx The 0-based frame index. 594 | * @param {string} frameRate Frame rate as a plain text string, with exactly 2 or 3 595 | * decimal digits of precision after the period (e.g. "23.976" or "24.00"). 596 | * @param {string} dropType "drop" or "non-drop". 597 | * @return {string} Timecode of given frameIdx. 598 | * @customFunction 599 | */ 600 | function FRAMEIDX_TO_TC(frameIdx, frameRate, dropType) { 601 | const tcStd = parseTcStd_(frameRate, dropType); 602 | return frameIdxToTc_(frameIdx, tcStd); 603 | } 604 | 605 | /** 606 | * @param {number} frameIdx 607 | * @param {TimecodeStandard} tcStd 608 | * @return {string} 609 | * @private 610 | */ 611 | function frameIdxToTc_(frameIdx, tcStd) { 612 | if (frameIdx < 0) { 613 | throw inputValueErr_('negative timecode values are not supported'); 614 | } 615 | 616 | const framesPerMin = tcStd.intFps * SECS_PER_MIN_; 617 | const framesPerHr = framesPerMin * MINS_PER_HR_; 618 | 619 | // If this is a drop frame standard, adjust for any dropped frames. 620 | let framesRemaining = frameIdx + framesDroppedBeforeFrameIdx_(frameIdx, tcStd); 621 | 622 | const hh = Math.floor(framesRemaining / framesPerHr); 623 | framesRemaining -= hh * framesPerHr; 624 | 625 | const mm = Math.floor(framesRemaining / framesPerMin); 626 | framesRemaining -= mm * framesPerMin; 627 | 628 | const ss = Math.floor(framesRemaining / tcStd.intFps); 629 | framesRemaining -= ss * tcStd.intFps; 630 | 631 | const ff = framesRemaining; 632 | 633 | return tcToStr_({hh: hh, mm: mm, ss: ss, ff: ff}); 634 | } 635 | 636 | /** 637 | * @param {ParsedTimecode} tc 638 | * @return {string} 639 | * @private 640 | */ 641 | function tcToStr_(tc) { 642 | const hh = String(tc.hh).padStart(2, '0'); 643 | const mm = String(tc.mm).padStart(2, '0'); 644 | const ss = String(tc.ss).padStart(2, '0'); 645 | const ff = String(tc.ff).padStart(2, '0'); 646 | 647 | // TODO: Optionally support ';' separator for drop frame standards, if this 648 | // feature is sufficiently requested. 649 | return `${hh}:${mm}:${ss}:${ff}`; 650 | } 651 | 652 | /** 653 | * @param {number} frameIdx 654 | * @param {TimecodeStandard} tcStd 655 | * @return {number} 656 | * @private 657 | */ 658 | function framesDroppedBeforeFrameIdx_(frameIdx, tcStd) { 659 | if (tcStd.dropFramesPer10Mins === 0) { 660 | return 0; 661 | } 662 | 663 | const framesPerNonDropMin = tcStd.intFps * SECS_PER_MIN_; 664 | const framesPerDroppedBlock = framesPerDroppedBlock_(tcStd) 665 | const framesPerDropMin = framesPerNonDropMin - framesPerDroppedBlock; 666 | 667 | // Count # of full blocks of 10 minutes (of timecode, not wall time). 668 | const framesPer10Mins = 10 * framesPerNonDropMin - tcStd.dropFramesPer10Mins; 669 | 670 | let framesRemaining = frameIdx; 671 | const numComplete10MinBlocks = Math.floor(framesRemaining / framesPer10Mins); 672 | framesRemaining -= framesPer10Mins * numComplete10MinBlocks; 673 | 674 | let numDroppedFrames = numComplete10MinBlocks * tcStd.dropFramesPer10Mins; 675 | 676 | if (framesRemaining >= framesPerNonDropMin) { 677 | // First minute of this 10 minute block has no dropped frames. 678 | framesRemaining -= framesPerNonDropMin; 679 | 680 | // Each complete drop minute plus the current minute drops one block of frames. 681 | const numCompleteDropMins = Math.floor(framesRemaining / framesPerDropMin); 682 | numDroppedFrames += (numCompleteDropMins + 1) * framesPerDroppedBlock; 683 | } 684 | 685 | return numDroppedFrames; 686 | } 687 | 688 | 689 | //================================================================================================== 690 | // Module Exports 691 | //================================================================================================== 692 | 693 | if ((typeof module !== 'undefined') && module.exports) { 694 | module.exports = { 695 | FRAMEIDX_TO_TC: FRAMEIDX_TO_TC, 696 | FRAMEIDX_TO_WALL_SECS: FRAMEIDX_TO_WALL_SECS, 697 | TC_ERROR: TC_ERROR, 698 | TC_TO_FRAMEIDX: TC_TO_FRAMEIDX, 699 | TC_TO_WALL_SECS: TC_TO_WALL_SECS, 700 | WALL_SECS_BETWEEN_TCS: WALL_SECS_BETWEEN_TCS, 701 | WALL_SECS_TO_DURSTR: WALL_SECS_TO_DURSTR, 702 | WALL_SECS_TO_FRAMEIDX_LEFT: WALL_SECS_TO_FRAMEIDX_LEFT, 703 | WALL_SECS_TO_FRAMEIDX_RIGHT: WALL_SECS_TO_FRAMEIDX_RIGHT, 704 | WALL_SECS_TO_TC_LEFT: WALL_SECS_TO_TC_LEFT, 705 | WALL_SECS_TO_TC_RIGHT: WALL_SECS_TO_TC_RIGHT, 706 | }; 707 | } 708 | -------------------------------------------------------------------------------- /Code.test.js: -------------------------------------------------------------------------------- 1 | const Code = require('./Code'); 2 | 3 | const PRECISION_8DIGITS = 8; 4 | 5 | describe('TC_ERROR', () => { 6 | it('rejects invalid timecode standards', () => { 7 | expect(Code.TC_ERROR('01:02:03:04', 24, 'non-drop')) 8 | .toContain('frameRate must be a single plain text value'); 9 | expect(Code.TC_ERROR('01:02:03:04', '24', 'non-drop')) 10 | .toContain('frameRate must contain 2 or 3 digits after period'); 11 | expect(Code.TC_ERROR('01:02:03:04', '12.00', 'non-drop')) 12 | .toContain('Unsupported frame rate: "12.00"'); 13 | expect(Code.TC_ERROR('01:02:03:04', '96.000', 'non-drop')) 14 | .toContain('Unsupported frame rate: "96.000"'); 15 | 16 | expect(Code.TC_ERROR('01:02:03:04', '25.000', true)) 17 | .toContain('dropType must be a single plain text value'); 18 | expect(Code.TC_ERROR('01:02:03:04', '25.000', 'd')) 19 | .toContain('dropType value must be "non-drop" or "drop"'); 20 | expect(Code.TC_ERROR('01:02:03:04', '25.000', 'nondrop')) 21 | .toContain('dropType value must be "non-drop" or "drop"'); 22 | 23 | expect(Code.TC_ERROR('01:02:03:04', '23.976', 'drop')) 24 | .toContain('frameRate 23.976 must be non-drop'); 25 | }); 26 | 27 | it('accepts valid timecode standards', () => { 28 | expect(Code.TC_ERROR('01:02:03:04', ' 24.00 ', 'non-drop')).toBe(''); 29 | expect(Code.TC_ERROR('01:02:03:04', '60.000', 'NoN-DRop')).toBe(''); 30 | expect(Code.TC_ERROR('01:02:03:04', '29.97', 'drop')).toBe(''); 31 | expect(Code.TC_ERROR('01:02:03:04', '59.940', ' drOP ')).toBe(''); 32 | expect(Code.TC_ERROR('01:02:03:04', '48.000', 'non-drop')).toBe(''); 33 | }); 34 | 35 | it('rejects invalid format timecode', () => { 36 | expect(Code.TC_ERROR(Number.NEGATIVE_INFINITY, '24.00', 'non-drop')) 37 | .toContain('numerical timecode must be an integer in [0, 99999999] range'); 38 | expect(Code.TC_ERROR(Number.NaN, '24.00', 'non-drop')) 39 | .toContain('numerical timecode must be an integer in [0, 99999999] range'); 40 | expect(Code.TC_ERROR(Number.POSITIVE_INFINITY, '24.00', 'non-drop')) 41 | .toContain('numerical timecode must be an integer in [0, 99999999] range'); 42 | 43 | expect(Code.TC_ERROR(34.56, '24.00', 'non-drop')) 44 | .toContain('numerical timecode must be an integer in [0, 99999999] range'); 45 | 46 | expect(Code.TC_ERROR(-1, '24.00', 'non-drop')) 47 | .toContain('numerical timecode must be an integer in [0, 99999999] range'); 48 | expect(Code.TC_ERROR(100000000, '24.00', 'non-drop')) 49 | .toContain('numerical timecode must be an integer in [0, 99999999] range'); 50 | 51 | expect(Code.TC_ERROR('01020304', '24.00', 'non-drop')) 52 | .toContain('timecode must be in HH:MM:SS:FF format: "01020304"'); 53 | expect(Code.TC_ERROR('1:2:3:4', '24.00', 'non-drop')) 54 | .toContain('timecode must be in HH:MM:SS:FF format: "1:2:3:4"'); 55 | 56 | expect(Code.TC_ERROR('00:00:00;00', '29.97', 'non-drop')) 57 | .toContain('only drop timecode may use semi-colon separator: "00:00:00;00"'); 58 | expect(Code.TC_ERROR('01;02;03;04', '59.940', 'non-drop')) 59 | .toContain('only drop timecode may use semi-colon separator: "01;02;03;04"'); 60 | }); 61 | 62 | it('rejects out of range MM, SS, and FF values', () => { 63 | expect(Code.TC_ERROR('00:60:00:00', '24.000', 'non-drop')) 64 | .toContain('timecode MM must be in range 00-59: "60"'); 65 | expect(Code.TC_ERROR('00:99:00:00', '60.000', 'non-drop')) 66 | .toContain('timecode MM must be in range 00-59: "99"'); 67 | 68 | expect(Code.TC_ERROR('00:00:60:00', '24.000', 'non-drop')) 69 | .toContain('timecode SS must be in range 00-59: "60"'); 70 | expect(Code.TC_ERROR('00:00:99:00', '60.000', 'non-drop')) 71 | .toContain('timecode SS must be in range 00-59: "99"'); 72 | 73 | expect(Code.TC_ERROR('00:00:00:24', '23.976', 'non-drop')) 74 | .toContain('timecode FF must be in range 00-23: "24"'); 75 | expect(Code.TC_ERROR('00:00:00:30', '29.97', 'drop')) 76 | .toContain('timecode FF must be in range 00-29: "30"'); 77 | }); 78 | 79 | it('rejects invalid dropped frames', () => { 80 | expect(Code.TC_ERROR('00:01:00:00', '29.97', 'drop')) 81 | .toContain('timecode invalid: "00:01:00:00" is a dropped frame number'); 82 | expect(Code.TC_ERROR('07:33:00:01', '29.970', 'drop')) 83 | .toContain('timecode invalid: "07:33:00:01" is a dropped frame number'); 84 | 85 | expect(Code.TC_ERROR('00:01:00:02', '59.94', 'drop')) 86 | .toContain('timecode invalid: "00:01:00:02" is a dropped frame number'); 87 | expect(Code.TC_ERROR('07:33:00:03', '59.94', 'drop')) 88 | .toContain('timecode invalid: "07:33:00:03" is a dropped frame number'); 89 | }); 90 | 91 | it('accepts valid format timecode (string format)', () => { 92 | expect(Code.TC_ERROR(' 11:22:33:44 ', '60.000', 'non-drop')).toBe(''); 93 | 94 | expect(Code.TC_ERROR('00:00:00:23', '23.976', 'non-drop')).toBe(''); 95 | expect(Code.TC_ERROR('01:02:03:29', '29.97', 'drop')).toBe(''); 96 | expect(Code.TC_ERROR('00:01:00:02', '29.970', 'drop')).toBe(''); 97 | expect(Code.TC_ERROR('07:33:00:03', '29.97', 'drop')).toBe(''); 98 | 99 | expect(Code.TC_ERROR('00:00:00;00', '29.97', 'drop')).toBe(''); 100 | expect(Code.TC_ERROR('01;02;03;04', '59.940', 'drop')).toBe(''); 101 | }); 102 | 103 | it('accepts valid format timecode (number format)', () => { 104 | expect(Code.TC_ERROR(11223344, '60.00', 'non-drop')).toBe(''); 105 | 106 | expect(Code.TC_ERROR(23, '23.976', 'non-drop')).toBe(''); 107 | expect(Code.TC_ERROR(1020329, '29.97', 'drop')).toBe(''); 108 | expect(Code.TC_ERROR(10002, '29.970', 'drop')).toBe(''); 109 | expect(Code.TC_ERROR(7330003, '29.97', 'drop')).toBe(''); 110 | 111 | expect(Code.TC_ERROR(0, '29.97', 'drop')).toBe(''); 112 | expect(Code.TC_ERROR(1020304, '59.940', 'drop')).toBe(''); 113 | }); 114 | }); 115 | 116 | describe('TC_TO_FRAMEIDX', () => { 117 | it('rejects invalid timecode values and timecode standards', () => { 118 | expect(() => Code.TC_TO_FRAMEIDX('1:2:3:4', '24.00', 'non-drop')) 119 | .toThrow(/timecode must be in HH:MM:SS:FF format: "1:2:3:4"/); 120 | expect(() => Code.TC_TO_FRAMEIDX('00:00:00:30', '29.97', 'drop')) 121 | .toThrow(/timecode FF must be in range 00-29: "30"/); 122 | expect(() => Code.TC_TO_FRAMEIDX('00:01:00:02', '59.94', 'drop')) 123 | .toThrow(/timecode invalid: "00:01:00:02" is a dropped frame number/); 124 | 125 | expect(() => Code.TC_TO_FRAMEIDX('01:02:03:04', '12.00', 'non-drop')) 126 | .toThrow(/Unsupported frame rate: "12.00"/); 127 | expect(() => Code.TC_TO_FRAMEIDX('01:02:03:04', '25.000', 'nondrop')) 128 | .toThrow(/dropType value must be "non-drop" or "drop"/); 129 | }); 130 | 131 | it('converts non-drop frames correctly (string format)', () => { 132 | expect(Code.TC_TO_FRAMEIDX('00:00:00:00', '24.00', 'non-drop')).toBe(0); 133 | expect(Code.TC_TO_FRAMEIDX('00:00:00:01', '29.97', 'non-drop')).toBe(1); 134 | expect(Code.TC_TO_FRAMEIDX('00:00:00:02', '50.000', 'non-drop')).toBe(2); 135 | 136 | expect(Code.TC_TO_FRAMEIDX('00:00:01:00', '24.00', 'non-drop')).toBe(24); 137 | expect(Code.TC_TO_FRAMEIDX('00:00:01:01', '29.97', 'non-drop')).toBe(31); 138 | expect(Code.TC_TO_FRAMEIDX('00:00:01:02', '50.000', 'non-drop')).toBe(52); 139 | 140 | // 160,402 timecode seconds plus 11 frames: 141 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '23.976', 'non-drop')).toBe(3849659); 142 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '23.98', 'non-drop')).toBe(3849659); 143 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '24.000', 'non-drop')).toBe(3849659); 144 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '24.00', 'non-drop')).toBe(3849659); 145 | 146 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '25.000', 'non-drop')).toBe(4010061); 147 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '25.00', 'non-drop')).toBe(4010061); 148 | 149 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '29.970', 'non-drop')).toBe(4812071); 150 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '29.97', 'non-drop')).toBe(4812071); 151 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '30.000', 'non-drop')).toBe(4812071); 152 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '30.00', 'non-drop')).toBe(4812071); 153 | 154 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '47.952', 'non-drop')).toBe(7699307); 155 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '47.95', 'non-drop')).toBe(7699307); 156 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '48.000', 'non-drop')).toBe(7699307); 157 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '48.00', 'non-drop')).toBe(7699307); 158 | 159 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '50.000', 'non-drop')).toBe(8020111); 160 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '50.00', 'non-drop')).toBe(8020111); 161 | 162 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '59.940', 'non-drop')).toBe(9624131); 163 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '59.94', 'non-drop')).toBe(9624131); 164 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '60.000', 'non-drop')).toBe(9624131); 165 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '60.00', 'non-drop')).toBe(9624131); 166 | }); 167 | 168 | it('converts non-drop frames correctly (number format)', () => { 169 | expect(Code.TC_TO_FRAMEIDX(0, '24.00', 'non-drop')).toBe(0); 170 | expect(Code.TC_TO_FRAMEIDX(1, '29.97', 'non-drop')).toBe(1); 171 | expect(Code.TC_TO_FRAMEIDX(2, '50.000', 'non-drop')).toBe(2); 172 | 173 | expect(Code.TC_TO_FRAMEIDX(100, '24.00', 'non-drop')).toBe(24); 174 | expect(Code.TC_TO_FRAMEIDX(101, '29.97', 'non-drop')).toBe(31); 175 | expect(Code.TC_TO_FRAMEIDX(102, '50.000', 'non-drop')).toBe(52); 176 | 177 | // 160,402 timecode seconds plus 11 frames: 178 | expect(Code.TC_TO_FRAMEIDX(44332211, '23.976', 'non-drop')).toBe(3849659); 179 | expect(Code.TC_TO_FRAMEIDX(44332211, '23.98', 'non-drop')).toBe(3849659); 180 | expect(Code.TC_TO_FRAMEIDX(44332211, '24.000', 'non-drop')).toBe(3849659); 181 | expect(Code.TC_TO_FRAMEIDX(44332211, '24.00', 'non-drop')).toBe(3849659); 182 | 183 | expect(Code.TC_TO_FRAMEIDX(44332211, '25.000', 'non-drop')).toBe(4010061); 184 | expect(Code.TC_TO_FRAMEIDX(44332211, '25.00', 'non-drop')).toBe(4010061); 185 | 186 | expect(Code.TC_TO_FRAMEIDX(44332211, '29.970', 'non-drop')).toBe(4812071); 187 | expect(Code.TC_TO_FRAMEIDX(44332211, '29.97', 'non-drop')).toBe(4812071); 188 | expect(Code.TC_TO_FRAMEIDX(44332211, '30.000', 'non-drop')).toBe(4812071); 189 | expect(Code.TC_TO_FRAMEIDX(44332211, '30.00', 'non-drop')).toBe(4812071); 190 | 191 | expect(Code.TC_TO_FRAMEIDX(44332211, '47.952', 'non-drop')).toBe(7699307); 192 | expect(Code.TC_TO_FRAMEIDX(44332211, '47.95', 'non-drop')).toBe(7699307); 193 | expect(Code.TC_TO_FRAMEIDX(44332211, '48.000', 'non-drop')).toBe(7699307); 194 | expect(Code.TC_TO_FRAMEIDX(44332211, '48.00', 'non-drop')).toBe(7699307); 195 | 196 | expect(Code.TC_TO_FRAMEIDX(44332211, '50.000', 'non-drop')).toBe(8020111); 197 | expect(Code.TC_TO_FRAMEIDX(44332211, '50.00', 'non-drop')).toBe(8020111); 198 | 199 | expect(Code.TC_TO_FRAMEIDX(44332211, '59.940', 'non-drop')).toBe(9624131); 200 | expect(Code.TC_TO_FRAMEIDX(44332211, '59.94', 'non-drop')).toBe(9624131); 201 | expect(Code.TC_TO_FRAMEIDX(44332211, '60.000', 'non-drop')).toBe(9624131); 202 | expect(Code.TC_TO_FRAMEIDX(44332211, '60.00', 'non-drop')).toBe(9624131); 203 | }); 204 | 205 | it('converts drop frames correctly (string format)', () => { 206 | expect(Code.TC_TO_FRAMEIDX('00:00:59:29', '29.97', 'drop')).toBe(1799); 207 | expect(Code.TC_TO_FRAMEIDX('00:01:00;02', '29.97', 'drop')).toBe(1800); 208 | 209 | // 6*44 + 3 = 267 blocks of 10 minutes, plus 00:03:22:11: 210 | 211 | // 29.97 drop: 17,982 frames per 10 minutes (267 * 17,982 = 4,801,194), 212 | // plus 00:03:22:11 (202s*30/s + 11 = 6,071 frames; minus 3m * 2/m = 6 dropped), 213 | // total 4,801,194 + 6,071 - 6 = 4,807,259. 214 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '29.970', 'drop')).toBe(4807259); 215 | expect(Code.TC_TO_FRAMEIDX('44:33:22;11', '29.97', 'drop')).toBe(4807259); 216 | 217 | // 59.94 drop: 35,964 frames per 10 minutes (267 * 35,964 = 9,602,388), 218 | // plus 00:03:22:11 (202s*60/s + 11 = 12,131 frames; minus 3m * 4/m = 12 dropped), 219 | // total 9,602,388 + 12,131 - 12 = 9,614,507. 220 | expect(Code.TC_TO_FRAMEIDX('44:33:22:11', '59.940', 'drop')).toBe(9614507); 221 | expect(Code.TC_TO_FRAMEIDX('44;33;22;11', '59.94', 'drop')).toBe(9614507); 222 | }); 223 | 224 | it('converts drop frames correctly (number format)', () => { 225 | expect(Code.TC_TO_FRAMEIDX(5929, '29.97', 'drop')).toBe(1799); 226 | expect(Code.TC_TO_FRAMEIDX(10002, '29.97', 'drop')).toBe(1800); 227 | 228 | // 6*44 + 3 = 267 blocks of 10 minutes, plus 00:03:22:11: 229 | 230 | // 29.97 drop: 17,982 frames per 10 minutes (267 * 17,982 = 4,801,194), 231 | // plus 00:03:22:11 (202s*30/s + 11 = 6,071 frames; minus 3m * 2/m = 6 dropped), 232 | // total 4,801,194 + 6,071 - 6 = 4,807,259. 233 | expect(Code.TC_TO_FRAMEIDX(44332211, '29.970', 'drop')).toBe(4807259); 234 | expect(Code.TC_TO_FRAMEIDX(44332211, '29.97', 'drop')).toBe(4807259); 235 | 236 | // 59.94 drop: 35,964 frames per 10 minutes (267 * 35,964 = 9,602,388), 237 | // plus 00:03:22:11 (202s*60/s + 11 = 12,131 frames; minus 3m * 4/m = 12 dropped), 238 | // total 9,602,388 + 12,131 - 12 = 9,614,507. 239 | expect(Code.TC_TO_FRAMEIDX(44332211, '59.940', 'drop')).toBe(9614507); 240 | expect(Code.TC_TO_FRAMEIDX(44332211, '59.94', 'drop')).toBe(9614507); 241 | }); 242 | }); 243 | 244 | describe('FRAMEIDX_TO_WALL_SECS', () => { 245 | it('rejects invalid frameIdx values and timecode standards', () => { 246 | expect(() => Code.FRAMEIDX_TO_WALL_SECS(-1, '60.000', 'non-drop')) 247 | .toThrow(/frameIdx must be non-negative integer/); 248 | 249 | expect(() => Code.FRAMEIDX_TO_WALL_SECS(27, '12.00', 'non-drop')) 250 | .toThrow(/Unsupported frame rate: "12.00"/); 251 | expect(() => Code.FRAMEIDX_TO_WALL_SECS(27, '25.000', 'nondrop')) 252 | .toThrow(/dropType value must be "non-drop" or "drop"/); 253 | }); 254 | 255 | it('converts frame indexes correctly', () => { 256 | expect(Code.FRAMEIDX_TO_WALL_SECS(52, '50.00', 'non-drop')) 257 | .toBeCloseTo(1.04, PRECISION_8DIGITS); 258 | 259 | expect(Code.FRAMEIDX_TO_WALL_SECS(1799, '29.97', 'drop')) 260 | .toBeCloseTo(60.02663333, PRECISION_8DIGITS); 261 | expect(Code.FRAMEIDX_TO_WALL_SECS(1800, '29.97', 'drop')) 262 | .toBeCloseTo(60.06, PRECISION_8DIGITS); 263 | 264 | // 44:33:22:11 => 160,402 timecode seconds plus 11 frames: 265 | 266 | expect(Code.FRAMEIDX_TO_WALL_SECS(3849659, '23.976', 'non-drop')) 267 | .toBeCloseTo(160562.86079167, PRECISION_8DIGITS); 268 | expect(Code.FRAMEIDX_TO_WALL_SECS(3849659, '23.98', 'non-drop')) 269 | .toBeCloseTo(160562.86079167, PRECISION_8DIGITS); 270 | 271 | expect(Code.FRAMEIDX_TO_WALL_SECS(3849659, '24.000', 'non-drop')) 272 | .toBeCloseTo(160402.45833333, PRECISION_8DIGITS); 273 | expect(Code.FRAMEIDX_TO_WALL_SECS(3849659, '24.00', 'non-drop')) 274 | .toBeCloseTo(160402.45833333, PRECISION_8DIGITS); 275 | 276 | expect(Code.FRAMEIDX_TO_WALL_SECS(4010061, '25.000', 'non-drop')) 277 | .toBeCloseTo(160402.44, PRECISION_8DIGITS); 278 | expect(Code.FRAMEIDX_TO_WALL_SECS(4010061, '25.00', 'non-drop')) 279 | .toBeCloseTo(160402.44, PRECISION_8DIGITS); 280 | 281 | expect(Code.FRAMEIDX_TO_WALL_SECS(4812071, '29.970', 'non-drop')) 282 | .toBeCloseTo(160562.76903333, PRECISION_8DIGITS); 283 | expect(Code.FRAMEIDX_TO_WALL_SECS(4812071, '29.97', 'non-drop')) 284 | .toBeCloseTo(160562.76903333, PRECISION_8DIGITS); 285 | 286 | expect(Code.FRAMEIDX_TO_WALL_SECS(4812071, '30.000', 'non-drop')) 287 | .toBeCloseTo(160402.36666667, PRECISION_8DIGITS); 288 | expect(Code.FRAMEIDX_TO_WALL_SECS(4812071, '30.00', 'non-drop')) 289 | .toBeCloseTo(160402.36666667, PRECISION_8DIGITS); 290 | 291 | expect(Code.FRAMEIDX_TO_WALL_SECS(7699307, '47.952', 'non-drop')) 292 | .toBeCloseTo(160562.63139583, PRECISION_8DIGITS); 293 | expect(Code.FRAMEIDX_TO_WALL_SECS(7699307, '47.95', 'non-drop')) 294 | .toBeCloseTo(160562.63139583, PRECISION_8DIGITS); 295 | 296 | expect(Code.FRAMEIDX_TO_WALL_SECS(7699307, '48.000', 'non-drop')) 297 | .toBeCloseTo(160402.22916667, PRECISION_8DIGITS); 298 | expect(Code.FRAMEIDX_TO_WALL_SECS(7699307, '48.00', 'non-drop')) 299 | .toBeCloseTo(160402.22916667, PRECISION_8DIGITS); 300 | 301 | expect(Code.FRAMEIDX_TO_WALL_SECS(8020111, '50.000', 'non-drop')) 302 | .toBeCloseTo(160402.22, PRECISION_8DIGITS); 303 | expect(Code.FRAMEIDX_TO_WALL_SECS(8020111, '50.00', 'non-drop')) 304 | .toBeCloseTo(160402.22, PRECISION_8DIGITS); 305 | 306 | expect(Code.FRAMEIDX_TO_WALL_SECS(9624131, '59.940', 'non-drop')) 307 | .toBeCloseTo(160562.58551667, PRECISION_8DIGITS); 308 | expect(Code.FRAMEIDX_TO_WALL_SECS(9624131, '59.94', 'non-drop')) 309 | .toBeCloseTo(160562.58551667, PRECISION_8DIGITS); 310 | 311 | expect(Code.FRAMEIDX_TO_WALL_SECS(9624131, '60.000', 'non-drop')) 312 | .toBeCloseTo(160402.18333333, PRECISION_8DIGITS); 313 | expect(Code.FRAMEIDX_TO_WALL_SECS(9624131, '60.00', 'non-drop')) 314 | .toBeCloseTo(160402.18333333, PRECISION_8DIGITS); 315 | 316 | expect(Code.FRAMEIDX_TO_WALL_SECS(4807259, '29.970', 'drop')) 317 | .toBeCloseTo(160402.20863333, PRECISION_8DIGITS); 318 | expect(Code.FRAMEIDX_TO_WALL_SECS(4807259, '29.97', 'drop')) 319 | .toBeCloseTo(160402.20863333, PRECISION_8DIGITS); 320 | 321 | expect(Code.FRAMEIDX_TO_WALL_SECS(9614507, '59.940', 'drop')) 322 | .toBeCloseTo(160402.02511667, PRECISION_8DIGITS); 323 | expect(Code.FRAMEIDX_TO_WALL_SECS(9614507, '59.94', 'drop')) 324 | .toBeCloseTo(160402.02511667, PRECISION_8DIGITS); 325 | }); 326 | }); 327 | 328 | describe('TC_TO_WALL_SECS', () => { 329 | it('rejects invalid timecode values and timecode standards', () => { 330 | expect(() => Code.TC_TO_WALL_SECS('1:2:3:4', '24.00', 'non-drop')) 331 | .toThrow(/timecode must be in HH:MM:SS:FF format: "1:2:3:4"/); 332 | expect(() => Code.TC_TO_WALL_SECS('00:00:00:30', '29.97', 'drop')) 333 | .toThrow(/timecode FF must be in range 00-29: "30"/); 334 | expect(() => Code.TC_TO_WALL_SECS('00:01:00:02', '59.94', 'drop')) 335 | .toThrow(/timecode invalid: "00:01:00:02" is a dropped frame number/); 336 | 337 | expect(() => Code.TC_TO_WALL_SECS('01:02:03:04', '12.00', 'non-drop')) 338 | .toThrow(/Unsupported frame rate: "12.00"/); 339 | expect(() => Code.TC_TO_WALL_SECS('01:02:03:04', '25.000', 'nondrop')) 340 | .toThrow(/dropType value must be "non-drop" or "drop"/); 341 | }); 342 | 343 | it('converts to wall seconds correctly (string format)', () => { 344 | expect(Code.TC_TO_WALL_SECS('00:00:01:02', '50.00', 'non-drop')) 345 | .toBeCloseTo(1.04, PRECISION_8DIGITS); 346 | 347 | expect(Code.TC_TO_WALL_SECS('00:00:59:29', '29.97', 'drop')) 348 | .toBeCloseTo(60.02663333, PRECISION_8DIGITS); 349 | expect(Code.TC_TO_WALL_SECS('00:01:00;02', '29.97', 'drop')) 350 | .toBeCloseTo(60.06, PRECISION_8DIGITS); 351 | 352 | // 44:33:22:11 => 160,402 timecode seconds plus 11 frames: 353 | 354 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '23.976', 'non-drop')) 355 | .toBeCloseTo(160562.86079167, PRECISION_8DIGITS); 356 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '23.98', 'non-drop')) 357 | .toBeCloseTo(160562.86079167, PRECISION_8DIGITS); 358 | 359 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '24.000', 'non-drop')) 360 | .toBeCloseTo(160402.45833333, PRECISION_8DIGITS); 361 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '24.00', 'non-drop')) 362 | .toBeCloseTo(160402.45833333, PRECISION_8DIGITS); 363 | 364 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '25.000', 'non-drop')) 365 | .toBeCloseTo(160402.44, PRECISION_8DIGITS); 366 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '25.00', 'non-drop')) 367 | .toBeCloseTo(160402.44, PRECISION_8DIGITS); 368 | 369 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '29.970', 'non-drop')) 370 | .toBeCloseTo(160562.76903333, PRECISION_8DIGITS); 371 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '29.97', 'non-drop')) 372 | .toBeCloseTo(160562.76903333, PRECISION_8DIGITS); 373 | 374 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '30.000', 'non-drop')) 375 | .toBeCloseTo(160402.36666667, PRECISION_8DIGITS); 376 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '30.00', 'non-drop')) 377 | .toBeCloseTo(160402.36666667, PRECISION_8DIGITS); 378 | 379 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '47.952', 'non-drop')) 380 | .toBeCloseTo(160562.63139583, PRECISION_8DIGITS); 381 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '47.95', 'non-drop')) 382 | .toBeCloseTo(160562.63139583, PRECISION_8DIGITS); 383 | 384 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '48.000', 'non-drop')) 385 | .toBeCloseTo(160402.22916667, PRECISION_8DIGITS); 386 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '48.00', 'non-drop')) 387 | .toBeCloseTo(160402.22916667, PRECISION_8DIGITS); 388 | 389 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '50.000', 'non-drop')) 390 | .toBeCloseTo(160402.22, PRECISION_8DIGITS); 391 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '50.00', 'non-drop')) 392 | .toBeCloseTo(160402.22, PRECISION_8DIGITS); 393 | 394 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '59.940', 'non-drop')) 395 | .toBeCloseTo(160562.58551667, PRECISION_8DIGITS); 396 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '59.94', 'non-drop')) 397 | .toBeCloseTo(160562.58551667, PRECISION_8DIGITS); 398 | 399 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '60.000', 'non-drop')) 400 | .toBeCloseTo(160402.18333333, PRECISION_8DIGITS); 401 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '60.00', 'non-drop')) 402 | .toBeCloseTo(160402.18333333, PRECISION_8DIGITS); 403 | 404 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '29.970', 'drop')) 405 | .toBeCloseTo(160402.20863333, PRECISION_8DIGITS); 406 | expect(Code.TC_TO_WALL_SECS('44:33:22;11', '29.97', 'drop')) 407 | .toBeCloseTo(160402.20863333, PRECISION_8DIGITS); 408 | 409 | expect(Code.TC_TO_WALL_SECS('44;33;22;11', '59.940', 'drop')) 410 | .toBeCloseTo(160402.02511667, PRECISION_8DIGITS); 411 | expect(Code.TC_TO_WALL_SECS('44:33:22:11', '59.94', 'drop')) 412 | .toBeCloseTo(160402.02511667, PRECISION_8DIGITS); 413 | }); 414 | 415 | it('converts to wall seconds correctly (number format)', () => { 416 | expect(Code.TC_TO_WALL_SECS(102, '50.00', 'non-drop')) 417 | .toBeCloseTo(1.04, PRECISION_8DIGITS); 418 | 419 | expect(Code.TC_TO_WALL_SECS(5929, '29.97', 'drop')) 420 | .toBeCloseTo(60.02663333, PRECISION_8DIGITS); 421 | expect(Code.TC_TO_WALL_SECS(10002, '29.97', 'drop')) 422 | .toBeCloseTo(60.06, PRECISION_8DIGITS); 423 | 424 | // 44:33:22:11 => 160,402 timecode seconds plus 11 frames: 425 | 426 | expect(Code.TC_TO_WALL_SECS(44332211, '23.976', 'non-drop')) 427 | .toBeCloseTo(160562.86079167, PRECISION_8DIGITS); 428 | expect(Code.TC_TO_WALL_SECS(44332211, '23.98', 'non-drop')) 429 | .toBeCloseTo(160562.86079167, PRECISION_8DIGITS); 430 | 431 | expect(Code.TC_TO_WALL_SECS(44332211, '24.000', 'non-drop')) 432 | .toBeCloseTo(160402.45833333, PRECISION_8DIGITS); 433 | expect(Code.TC_TO_WALL_SECS(44332211, '24.00', 'non-drop')) 434 | .toBeCloseTo(160402.45833333, PRECISION_8DIGITS); 435 | 436 | expect(Code.TC_TO_WALL_SECS(44332211, '25.000', 'non-drop')) 437 | .toBeCloseTo(160402.44, PRECISION_8DIGITS); 438 | expect(Code.TC_TO_WALL_SECS(44332211, '25.00', 'non-drop')) 439 | .toBeCloseTo(160402.44, PRECISION_8DIGITS); 440 | 441 | expect(Code.TC_TO_WALL_SECS(44332211, '29.970', 'non-drop')) 442 | .toBeCloseTo(160562.76903333, PRECISION_8DIGITS); 443 | expect(Code.TC_TO_WALL_SECS(44332211, '29.97', 'non-drop')) 444 | .toBeCloseTo(160562.76903333, PRECISION_8DIGITS); 445 | 446 | expect(Code.TC_TO_WALL_SECS(44332211, '30.000', 'non-drop')) 447 | .toBeCloseTo(160402.36666667, PRECISION_8DIGITS); 448 | expect(Code.TC_TO_WALL_SECS(44332211, '30.00', 'non-drop')) 449 | .toBeCloseTo(160402.36666667, PRECISION_8DIGITS); 450 | 451 | expect(Code.TC_TO_WALL_SECS(44332211, '47.952', 'non-drop')) 452 | .toBeCloseTo(160562.63139583, PRECISION_8DIGITS); 453 | expect(Code.TC_TO_WALL_SECS(44332211, '47.95', 'non-drop')) 454 | .toBeCloseTo(160562.63139583, PRECISION_8DIGITS); 455 | 456 | expect(Code.TC_TO_WALL_SECS(44332211, '48.000', 'non-drop')) 457 | .toBeCloseTo(160402.22916667, PRECISION_8DIGITS); 458 | expect(Code.TC_TO_WALL_SECS(44332211, '48.00', 'non-drop')) 459 | .toBeCloseTo(160402.22916667, PRECISION_8DIGITS); 460 | 461 | expect(Code.TC_TO_WALL_SECS(44332211, '50.000', 'non-drop')) 462 | .toBeCloseTo(160402.22, PRECISION_8DIGITS); 463 | expect(Code.TC_TO_WALL_SECS(44332211, '50.00', 'non-drop')) 464 | .toBeCloseTo(160402.22, PRECISION_8DIGITS); 465 | 466 | expect(Code.TC_TO_WALL_SECS(44332211, '59.940', 'non-drop')) 467 | .toBeCloseTo(160562.58551667, PRECISION_8DIGITS); 468 | expect(Code.TC_TO_WALL_SECS(44332211, '59.94', 'non-drop')) 469 | .toBeCloseTo(160562.58551667, PRECISION_8DIGITS); 470 | 471 | expect(Code.TC_TO_WALL_SECS(44332211, '60.000', 'non-drop')) 472 | .toBeCloseTo(160402.18333333, PRECISION_8DIGITS); 473 | expect(Code.TC_TO_WALL_SECS(44332211, '60.00', 'non-drop')) 474 | .toBeCloseTo(160402.18333333, PRECISION_8DIGITS); 475 | 476 | expect(Code.TC_TO_WALL_SECS(44332211, '29.970', 'drop')) 477 | .toBeCloseTo(160402.20863333, PRECISION_8DIGITS); 478 | expect(Code.TC_TO_WALL_SECS(44332211, '29.97', 'drop')) 479 | .toBeCloseTo(160402.20863333, PRECISION_8DIGITS); 480 | 481 | expect(Code.TC_TO_WALL_SECS(44332211, '59.940', 'drop')) 482 | .toBeCloseTo(160402.02511667, PRECISION_8DIGITS); 483 | expect(Code.TC_TO_WALL_SECS(44332211, '59.94', 'drop')) 484 | .toBeCloseTo(160402.02511667, PRECISION_8DIGITS); 485 | }); 486 | }); 487 | 488 | describe('WALL_SECS_BETWEEN_TCS', () => { 489 | it('rejects invalid timecode values and timecode standards', () => { 490 | expect(() => Code.WALL_SECS_BETWEEN_TCS('44:33:22:11', '1:2:3:4', '24.00', 'non-drop')) 491 | .toThrow(/timecode must be in HH:MM:SS:FF format: "1:2:3:4"/); 492 | expect(() => Code.WALL_SECS_BETWEEN_TCS('00:00:00:30', '44:33:22:11', '29.97', 'drop')) 493 | .toThrow(/timecode FF must be in range 00-29: "30"/); 494 | expect(() => Code.WALL_SECS_BETWEEN_TCS('44:33:22:11', '00:01:00:02', '59.94', 'drop')) 495 | .toThrow(/timecode invalid: "00:01:00:02" is a dropped frame number/); 496 | 497 | expect(() => Code.WALL_SECS_BETWEEN_TCS('01:02:03:04', '44:33:22:11', '12.00', 'non-drop')) 498 | .toThrow(/Unsupported frame rate: "12.00"/); 499 | expect(() => Code.WALL_SECS_BETWEEN_TCS('01:02:03:04', '44:33:22:11', '25.000', 'nondrop')) 500 | .toThrow(/dropType value must be "non-drop" or "drop"/); 501 | }); 502 | 503 | it('computes correct positive duration when start is <= end (string format)', () => { 504 | expect(Code.WALL_SECS_BETWEEN_TCS('00:00:00:00', '00:00:00:00', '23.976', 'non-drop')) 505 | .toBeCloseTo(0.0, PRECISION_8DIGITS); 506 | expect(Code.WALL_SECS_BETWEEN_TCS('44:33:22:11', '44:33:22:11', '23.976', 'non-drop')) 507 | .toBeCloseTo(0.0, PRECISION_8DIGITS); 508 | 509 | expect(Code.WALL_SECS_BETWEEN_TCS('44:33:22:11', '44:33:23:11', '23.976', 'non-drop')) 510 | .toBeCloseTo(1.001, PRECISION_8DIGITS); 511 | 512 | expect(Code.WALL_SECS_BETWEEN_TCS('00:00:01:03', '00:02:05:11', '24.000', 'non-drop')) 513 | .toBeCloseTo(124.33333333, PRECISION_8DIGITS); 514 | 515 | // Times are 415,853 frames apart: 516 | expect(Code.WALL_SECS_BETWEEN_TCS('44:33:22:11', '46:29:00:04', '59.940', 'drop')) 517 | .toBeCloseTo(6937.81421667, PRECISION_8DIGITS); 518 | 519 | // 1 frame less than 100 hours (360,000 seconds): 520 | expect(Code.WALL_SECS_BETWEEN_TCS('00:00:00:00', '99:59:59:59', '60.000', 'non-drop')) 521 | .toBeCloseTo(359999.98333333, PRECISION_8DIGITS); 522 | }); 523 | 524 | it('computes correct positive duration when start is <= end (number format)', () => { 525 | expect(Code.WALL_SECS_BETWEEN_TCS(0, 0, '23.976', 'non-drop')) 526 | .toBeCloseTo(0.0, PRECISION_8DIGITS); 527 | expect(Code.WALL_SECS_BETWEEN_TCS(44332211, 44332211, '23.976', 'non-drop')) 528 | .toBeCloseTo(0.0, PRECISION_8DIGITS); 529 | 530 | expect(Code.WALL_SECS_BETWEEN_TCS(44332211, 44332311, '23.976', 'non-drop')) 531 | .toBeCloseTo(1.001, PRECISION_8DIGITS); 532 | 533 | expect(Code.WALL_SECS_BETWEEN_TCS(103, 20511, '24.000', 'non-drop')) 534 | .toBeCloseTo(124.33333333, PRECISION_8DIGITS); 535 | 536 | // Times are 415,853 frames apart: 537 | expect(Code.WALL_SECS_BETWEEN_TCS(44332211, 46290004, '59.940', 'drop')) 538 | .toBeCloseTo(6937.81421667, PRECISION_8DIGITS); 539 | 540 | // 1 frame less than 100 hours (360,000 seconds): 541 | expect(Code.WALL_SECS_BETWEEN_TCS(0, 99595959, '60.000', 'non-drop')) 542 | .toBeCloseTo(359999.98333333, PRECISION_8DIGITS); 543 | }); 544 | 545 | it('computes correct negative duration when start is > end (string format)', () => { 546 | // Times are 415,853 frames apart: 547 | expect(Code.WALL_SECS_BETWEEN_TCS('46:29:00:04', '44:33:22:11', '59.940', 'drop')) 548 | .toBeCloseTo(-6937.81421667, PRECISION_8DIGITS); 549 | 550 | // 1 frame less than 100 hours (360,000 seconds): 551 | expect(Code.WALL_SECS_BETWEEN_TCS('99:59:59:59', '00:00:00:00', '60.000', 'non-drop')) 552 | .toBeCloseTo(-359999.98333333, PRECISION_8DIGITS); 553 | }); 554 | 555 | it('computes correct negative duration when start is > end (number format)', () => { 556 | // Times are 415,853 frames apart: 557 | expect(Code.WALL_SECS_BETWEEN_TCS(46290004, 44332211, '59.940', 'drop')) 558 | .toBeCloseTo(-6937.81421667, PRECISION_8DIGITS); 559 | 560 | // 1 frame less than 100 hours (360,000 seconds): 561 | expect(Code.WALL_SECS_BETWEEN_TCS(99595959, 0, '60.000', 'non-drop')) 562 | .toBeCloseTo(-359999.98333333, PRECISION_8DIGITS); 563 | }); 564 | }); 565 | 566 | describe('WALL_SECS_TO_DURSTR', () => { 567 | it('rejects non-finite number inputs', () => { 568 | expect(() => Code.WALL_SECS_TO_DURSTR('three')) 569 | .toThrow(/wallSecs must be a finite number/); 570 | expect(() => Code.WALL_SECS_TO_DURSTR(Number.NEGATIVE_INFINITY)) 571 | .toThrow(/wallSecs must be a finite number/); 572 | expect(() => Code.WALL_SECS_TO_DURSTR(Number.NaN)) 573 | .toThrow(/wallSecs must be a finite number/); 574 | expect(() => Code.WALL_SECS_TO_DURSTR(Number.POSITIVE_INFINITY)) 575 | .toThrow(/wallSecs must be a finite number/); 576 | }); 577 | 578 | it('works for positive durations', () => { 579 | expect(Code.WALL_SECS_TO_DURSTR(0)).toBe('00s'); 580 | expect(Code.WALL_SECS_TO_DURSTR(Number.EPSILON)).toBe('00s'); 581 | expect(Code.WALL_SECS_TO_DURSTR(0.49999999)).toBe('00s'); 582 | expect(Code.WALL_SECS_TO_DURSTR(0.5)).toBe('01s'); 583 | 584 | expect(Code.WALL_SECS_TO_DURSTR(1.0)).toBe('01s'); 585 | expect(Code.WALL_SECS_TO_DURSTR(1.5)).toBe('02s'); 586 | expect(Code.WALL_SECS_TO_DURSTR(59.49999999)).toBe('59s'); 587 | expect(Code.WALL_SECS_TO_DURSTR(59.5)).toBe('1m 00s'); 588 | expect(Code.WALL_SECS_TO_DURSTR(60.0)).toBe('1m 00s'); 589 | expect(Code.WALL_SECS_TO_DURSTR(60.49999999)).toBe('1m 00s'); 590 | 591 | expect(Code.WALL_SECS_TO_DURSTR(3540)).toBe('59m 00s'); 592 | expect(Code.WALL_SECS_TO_DURSTR(3599.49999999)).toBe('59m 59s'); 593 | expect(Code.WALL_SECS_TO_DURSTR(3600)).toBe('1h 00m 00s'); 594 | expect(Code.WALL_SECS_TO_DURSTR(3765)).toBe('1h 02m 45s'); 595 | 596 | expect(Code.WALL_SECS_TO_DURSTR(359999.49999999)).toBe('99h 59m 59s'); 597 | expect(Code.WALL_SECS_TO_DURSTR(359999.98333333)).toBe('100h 00m 00s'); 598 | }); 599 | 600 | it('works for negative durations', () => { 601 | expect(Code.WALL_SECS_TO_DURSTR(-0)).toBe('00s'); 602 | expect(Code.WALL_SECS_TO_DURSTR(-Number.EPSILON)).toBe('00s'); 603 | expect(Code.WALL_SECS_TO_DURSTR(-0.49999999)).toBe('00s'); 604 | expect(Code.WALL_SECS_TO_DURSTR(-0.5)).toBe('(-) 01s'); 605 | 606 | expect(Code.WALL_SECS_TO_DURSTR(-1.0)).toBe('(-) 01s'); 607 | expect(Code.WALL_SECS_TO_DURSTR(-1.5)).toBe('(-) 02s'); 608 | expect(Code.WALL_SECS_TO_DURSTR(-59.49999999)).toBe('(-) 59s'); 609 | expect(Code.WALL_SECS_TO_DURSTR(-59.5)).toBe('(-) 1m 00s'); 610 | expect(Code.WALL_SECS_TO_DURSTR(-60.0)).toBe('(-) 1m 00s'); 611 | expect(Code.WALL_SECS_TO_DURSTR(-60.49999999)).toBe('(-) 1m 00s'); 612 | 613 | expect(Code.WALL_SECS_TO_DURSTR(-3540)).toBe('(-) 59m 00s'); 614 | expect(Code.WALL_SECS_TO_DURSTR(-3599.49999999)).toBe('(-) 59m 59s'); 615 | expect(Code.WALL_SECS_TO_DURSTR(-3600)).toBe('(-) 1h 00m 00s'); 616 | expect(Code.WALL_SECS_TO_DURSTR(-3765)).toBe('(-) 1h 02m 45s'); 617 | 618 | expect(Code.WALL_SECS_TO_DURSTR(-359999.49999999)).toBe('(-) 99h 59m 59s'); 619 | expect(Code.WALL_SECS_TO_DURSTR(-359999.98333333)).toBe('(-) 100h 00m 00s'); 620 | }); 621 | }); 622 | 623 | describe('WALL_SECS_TO_FRAMEIDX_LEFT', () => { 624 | it('rejects invalid wallSecs and timecode standards', () => { 625 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_LEFT(Number.NEGATIVE_INFINITY, '25.00', 'non-drop')) 626 | .toThrow(/wallSecs must be a finite number/); 627 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_LEFT(Number.NaN, '25.00', 'non-drop')) 628 | .toThrow(/wallSecs must be a finite number/); 629 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_LEFT(Number.POSITIVE_INFINITY, '25.00', 'non-drop')) 630 | .toThrow(/wallSecs must be a finite number/); 631 | 632 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_LEFT(52, '12.00', 'non-drop')) 633 | .toThrow(/Unsupported frame rate: "12.00"/); 634 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_LEFT(52, '25.000', 'nondrop')) 635 | .toThrow(/dropType value must be "non-drop" or "drop"/); 636 | }); 637 | 638 | it('converts zero and positive wallSecs correctly', () => { 639 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(0, '23.976', 'non-drop')).toBe(0); 640 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(0, '24.00', 'non-drop')).toBe(0); 641 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(0, '50.00', 'non-drop')).toBe(0); 642 | 643 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(1.03999999, '50.00', 'non-drop')).toBe(51); 644 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(1.04, '50.00', 'non-drop')).toBe(52); 645 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(1.04000001, '50.00', 'non-drop')).toBe(52); 646 | 647 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(60.02663333, '29.97', 'drop')).toBe(1798); 648 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(60.02663334, '29.97', 'drop')).toBe(1799); 649 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(60.06, '29.97', 'drop')).toBe(1800); 650 | 651 | // 44:33:22:11 => 160,402 timecode seconds plus 11 frames: 652 | 653 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.86079166, '23.976', 'non-drop')).toBe(3849658); 654 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.86079166, '23.98', 'non-drop')).toBe(3849658); 655 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.86079167, '23.976', 'non-drop')).toBe(3849659); 656 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.86079167, '23.98', 'non-drop')).toBe(3849659); 657 | 658 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.45833333, '24.000', 'non-drop')).toBe(3849658); 659 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.45833333, '24.00', 'non-drop')).toBe(3849658); 660 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.45833334, '24.000', 'non-drop')).toBe(3849659); 661 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.45833334, '24.00', 'non-drop')).toBe(3849659); 662 | 663 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.43999999, '25.000', 'non-drop')).toBe(4010060); 664 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.43999999, '25.00', 'non-drop')).toBe(4010060); 665 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.44, '25.000', 'non-drop')).toBe(4010061); 666 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.44, '25.00', 'non-drop')).toBe(4010061); 667 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.44000001, '25.000', 'non-drop')).toBe(4010061); 668 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.44000001, '25.00', 'non-drop')).toBe(4010061); 669 | 670 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.76903333, '29.970', 'non-drop')).toBe(4812070); 671 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.76903333, '29.97', 'non-drop')).toBe(4812070); 672 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.76903334, '29.970', 'non-drop')).toBe(4812071); 673 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.76903334, '29.97', 'non-drop')).toBe(4812071); 674 | 675 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.36666666, '30.000', 'non-drop')).toBe(4812070); 676 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.36666666, '30.00', 'non-drop')).toBe(4812070); 677 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.36666667, '30.000', 'non-drop')).toBe(4812071); 678 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.36666667, '30.00', 'non-drop')).toBe(4812071); 679 | 680 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.63139583, '47.952', 'non-drop')).toBe(7699306); 681 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.63139583, '47.95', 'non-drop')).toBe(7699306); 682 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.63139584, '47.952', 'non-drop')).toBe(7699307); 683 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.63139584, '47.95', 'non-drop')).toBe(7699307); 684 | 685 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22916666, '48.000', 'non-drop')).toBe(7699306); 686 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22916666, '48.00', 'non-drop')).toBe(7699306); 687 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22916667, '48.000', 'non-drop')).toBe(7699307); 688 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22916667, '48.00', 'non-drop')).toBe(7699307); 689 | 690 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.21999999, '50.000', 'non-drop')).toBe(8020110); 691 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.21999999, '50.00', 'non-drop')).toBe(8020110); 692 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22, '50.000', 'non-drop')).toBe(8020111); 693 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22, '50.00', 'non-drop')).toBe(8020111); 694 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22000001, '50.000', 'non-drop')).toBe(8020111); 695 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.22000001, '50.00', 'non-drop')).toBe(8020111); 696 | 697 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.58551666, '59.940', 'non-drop')).toBe(9624130); 698 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.58551666, '59.94', 'non-drop')).toBe(9624130); 699 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.58551667, '59.940', 'non-drop')).toBe(9624131); 700 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160562.58551667, '59.94', 'non-drop')).toBe(9624131); 701 | 702 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.18333333, '60.000', 'non-drop')).toBe(9624130); 703 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.18333333, '60.00', 'non-drop')).toBe(9624130); 704 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.18333334, '60.000', 'non-drop')).toBe(9624131); 705 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.18333334, '60.00', 'non-drop')).toBe(9624131); 706 | 707 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.20863333, '29.970', 'drop')).toBe(4807258); 708 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.20863333, '29.97', 'drop')).toBe(4807258); 709 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.20863334, '29.970', 'drop')).toBe(4807259); 710 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.20863334, '29.97', 'drop')).toBe(4807259); 711 | 712 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.02511666, '59.940', 'drop')).toBe(9614506); 713 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.02511666, '59.94', 'drop')).toBe(9614506); 714 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.02511667, '59.940', 'drop')).toBe(9614507); 715 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(160402.02511667, '59.94', 'drop')).toBe(9614507); 716 | }); 717 | 718 | it('converts negative wallSecs correctly', () => { 719 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(-1.03999999, '50.00', 'non-drop')).toBe(-52); 720 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(-1.04, '50.00', 'non-drop')).toBe(-52); 721 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(-1.04000001, '50.00', 'non-drop')).toBe(-53); 722 | 723 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(-60.02663333, '29.97', 'drop')).toBe(-1799); 724 | expect(Code.WALL_SECS_TO_FRAMEIDX_LEFT(-60.02663334, '29.97', 'drop')).toBe(-1800); 725 | }); 726 | }); 727 | 728 | describe('WALL_SECS_TO_FRAMEIDX_RIGHT', () => { 729 | it('rejects invalid wallSecs and timecode standards', () => { 730 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_RIGHT(Number.NEGATIVE_INFINITY, '25.00', 'non-drop')) 731 | .toThrow(/wallSecs must be a finite number/); 732 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_RIGHT(Number.NaN, '25.00', 'non-drop')) 733 | .toThrow(/wallSecs must be a finite number/); 734 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_RIGHT(Number.POSITIVE_INFINITY, '25.00', 'non-drop')) 735 | .toThrow(/wallSecs must be a finite number/); 736 | 737 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_RIGHT(52, '12.00', 'non-drop')) 738 | .toThrow(/Unsupported frame rate: "12.00"/); 739 | expect(() => Code.WALL_SECS_TO_FRAMEIDX_RIGHT(52, '25.000', 'nondrop')) 740 | .toThrow(/dropType value must be "non-drop" or "drop"/); 741 | }); 742 | 743 | it('converts zero and positive wallSecs correctly', () => { 744 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(0, '23.976', 'non-drop')).toBe(0); 745 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(0, '24.00', 'non-drop')).toBe(0); 746 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(0, '50.00', 'non-drop')).toBe(0); 747 | 748 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(1.03999999, '50.00', 'non-drop')).toBe(52); 749 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(1.04, '50.00', 'non-drop')).toBe(52); 750 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(1.04000001, '50.00', 'non-drop')).toBe(53); 751 | 752 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(60.02663333, '29.97', 'drop')).toBe(1799); 753 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(60.02663334, '29.97', 'drop')).toBe(1800); 754 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(60.06, '29.97', 'drop')).toBe(1800); 755 | 756 | // 44:33:22:11 => 160,402 timecode seconds plus 11 frames: 757 | 758 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.86079166, '23.976', 'non-drop')).toBe(3849659); 759 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.86079166, '23.98', 'non-drop')).toBe(3849659); 760 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.86079167, '23.976', 'non-drop')).toBe(3849660); 761 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.86079167, '23.98', 'non-drop')).toBe(3849660); 762 | 763 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.45833333, '24.000', 'non-drop')).toBe(3849659); 764 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.45833333, '24.00', 'non-drop')).toBe(3849659); 765 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.45833334, '24.000', 'non-drop')).toBe(3849660); 766 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.45833334, '24.00', 'non-drop')).toBe(3849660); 767 | 768 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.43999999, '25.000', 'non-drop')).toBe(4010061); 769 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.43999999, '25.00', 'non-drop')).toBe(4010061); 770 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.44, '25.000', 'non-drop')).toBe(4010061); 771 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.44, '25.00', 'non-drop')).toBe(4010061); 772 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.44000001, '25.000', 'non-drop')).toBe(4010062); 773 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.44000001, '25.00', 'non-drop')).toBe(4010062); 774 | 775 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.76903333, '29.970', 'non-drop')).toBe(4812071); 776 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.76903333, '29.97', 'non-drop')).toBe(4812071); 777 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.76903334, '29.970', 'non-drop')).toBe(4812072); 778 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.76903334, '29.97', 'non-drop')).toBe(4812072); 779 | 780 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.36666666, '30.000', 'non-drop')).toBe(4812071); 781 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.36666666, '30.00', 'non-drop')).toBe(4812071); 782 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.36666667, '30.000', 'non-drop')).toBe(4812072); 783 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.36666667, '30.00', 'non-drop')).toBe(4812072); 784 | 785 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.63139583, '47.952', 'non-drop')).toBe(7699307); 786 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.63139583, '47.95', 'non-drop')).toBe(7699307); 787 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.63139584, '47.952', 'non-drop')).toBe(7699308); 788 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.63139584, '47.95', 'non-drop')).toBe(7699308); 789 | 790 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22916666, '48.000', 'non-drop')).toBe(7699307); 791 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22916666, '48.00', 'non-drop')).toBe(7699307); 792 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22916667, '48.000', 'non-drop')).toBe(7699308); 793 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22916667, '48.00', 'non-drop')).toBe(7699308); 794 | 795 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.21999999, '50.000', 'non-drop')).toBe(8020111); 796 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.21999999, '50.00', 'non-drop')).toBe(8020111); 797 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22, '50.000', 'non-drop')).toBe(8020111); 798 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22, '50.00', 'non-drop')).toBe(8020111); 799 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22000001, '50.000', 'non-drop')).toBe(8020112); 800 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.22000001, '50.00', 'non-drop')).toBe(8020112); 801 | 802 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.58551666, '59.940', 'non-drop')).toBe(9624131); 803 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.58551666, '59.94', 'non-drop')).toBe(9624131); 804 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.58551667, '59.940', 'non-drop')).toBe(9624132); 805 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160562.58551667, '59.94', 'non-drop')).toBe(9624132); 806 | 807 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.18333333, '60.000', 'non-drop')).toBe(9624131); 808 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.18333333, '60.00', 'non-drop')).toBe(9624131); 809 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.18333334, '60.000', 'non-drop')).toBe(9624132); 810 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.18333334, '60.00', 'non-drop')).toBe(9624132); 811 | 812 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.20863333, '29.970', 'drop')).toBe(4807259); 813 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.20863333, '29.97', 'drop')).toBe(4807259); 814 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.20863334, '29.970', 'drop')).toBe(4807260); 815 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.20863334, '29.97', 'drop')).toBe(4807260); 816 | 817 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.02511666, '59.940', 'drop')).toBe(9614507); 818 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.02511666, '59.94', 'drop')).toBe(9614507); 819 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.02511667, '59.940', 'drop')).toBe(9614508); 820 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(160402.02511667, '59.94', 'drop')).toBe(9614508); 821 | }); 822 | 823 | it('converts negative wallSecs correctly', () => { 824 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(-1.03999999, '50.00', 'non-drop')).toBe(-51); 825 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(-1.04, '50.00', 'non-drop')).toBe(-52); 826 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(-1.04000001, '50.00', 'non-drop')).toBe(-52); 827 | 828 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(-60.02663333, '29.97', 'drop')).toBe(-1798); 829 | expect(Code.WALL_SECS_TO_FRAMEIDX_RIGHT(-60.02663334, '29.97', 'drop')).toBe(-1799); 830 | }); 831 | }); 832 | 833 | describe('WALL_SECS_TO_TC_LEFT', () => { 834 | it('rejects invalid wallSecs values and timecode standards', () => { 835 | expect(() => Code.WALL_SECS_TO_TC_LEFT(-123.45, '60.000', 'non-drop')) 836 | .toThrow(/negative timecode values are not supported/); 837 | 838 | expect(() => Code.WALL_SECS_TO_TC_LEFT(123.45, '12.00', 'non-drop')) 839 | .toThrow(/Unsupported frame rate: "12.00"/); 840 | expect(() => Code.WALL_SECS_TO_TC_LEFT(123.45, '25.000', 'nondrop')) 841 | .toThrow(/dropType value must be "non-drop" or "drop"/); 842 | }); 843 | 844 | it('converts to wall seconds correctly', () => { 845 | expect(Code.WALL_SECS_TO_TC_LEFT(1.04, '50.00', 'non-drop')).toBe('00:00:01:02'); 846 | 847 | expect(Code.WALL_SECS_TO_TC_RIGHT(60.02663333, '29.97', 'drop')).toBe('00:00:59:29'); 848 | expect(Code.WALL_SECS_TO_TC_RIGHT(60.02663334, '29.97', 'drop')).toBe('00:01:00:02'); 849 | 850 | expect(Code.WALL_SECS_TO_TC_LEFT(60.02663333, '29.97', 'drop')).toBe('00:00:59:28'); 851 | expect(Code.WALL_SECS_TO_TC_LEFT(60.02663334, '29.97', 'drop')).toBe('00:00:59:29'); 852 | expect(Code.WALL_SECS_TO_TC_LEFT(60.06, '29.97', 'drop')).toBe('00:01:00:02'); 853 | 854 | // 44:33:22:11 => 160,402 timecode seconds plus 11 frames: 855 | 856 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.86079167, '23.976', 'non-drop')).toBe('44:33:22:11'); 857 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.86079167, '23.98', 'non-drop')).toBe('44:33:22:11'); 858 | 859 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.45833334, '24.000', 'non-drop')).toBe('44:33:22:11'); 860 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.45833334, '24.00', 'non-drop')).toBe('44:33:22:11'); 861 | 862 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.44, '25.000', 'non-drop')).toBe('44:33:22:11'); 863 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.44, '25.00', 'non-drop')).toBe('44:33:22:11'); 864 | 865 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.76903334, '29.970', 'non-drop')).toBe('44:33:22:11'); 866 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.76903334, '29.97', 'non-drop')).toBe('44:33:22:11'); 867 | 868 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.36666667, '30.000', 'non-drop')).toBe('44:33:22:11'); 869 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.36666667, '30.00', 'non-drop')).toBe('44:33:22:11'); 870 | 871 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.63139584, '47.952', 'non-drop')).toBe('44:33:22:11'); 872 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.63139584, '47.95', 'non-drop')).toBe('44:33:22:11'); 873 | 874 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.22916667, '48.000', 'non-drop')).toBe('44:33:22:11'); 875 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.22916667, '48.00', 'non-drop')).toBe('44:33:22:11'); 876 | 877 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.22, '50.000', 'non-drop')).toBe('44:33:22:11'); 878 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.22, '50.00', 'non-drop')).toBe('44:33:22:11'); 879 | 880 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.58551667, '59.940', 'non-drop')).toBe('44:33:22:11'); 881 | expect(Code.WALL_SECS_TO_TC_LEFT(160562.58551667, '59.94', 'non-drop')).toBe('44:33:22:11'); 882 | 883 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.18333334, '60.000', 'non-drop')).toBe('44:33:22:11'); 884 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.18333334, '60.00', 'non-drop')).toBe('44:33:22:11'); 885 | 886 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.20863334, '29.970', 'drop')).toBe('44:33:22:11'); 887 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.20863334, '29.97', 'drop')).toBe('44:33:22:11'); 888 | 889 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.02511667, '59.940', 'drop')).toBe('44:33:22:11'); 890 | expect(Code.WALL_SECS_TO_TC_LEFT(160402.02511667, '59.94', 'drop')).toBe('44:33:22:11'); 891 | }); 892 | }); 893 | 894 | describe('WALL_SECS_TO_TC_RIGHT', () => { 895 | it('rejects invalid wallSecs values and timecode standards', () => { 896 | expect(() => Code.WALL_SECS_TO_TC_RIGHT(-123.45, '60.000', 'non-drop')) 897 | .toThrow(/negative timecode values are not supported/); 898 | 899 | expect(() => Code.WALL_SECS_TO_TC_RIGHT(123.45, '12.00', 'non-drop')) 900 | .toThrow(/Unsupported frame rate: "12.00"/); 901 | expect(() => Code.WALL_SECS_TO_TC_RIGHT(123.45, '25.000', 'nondrop')) 902 | .toThrow(/dropType value must be "non-drop" or "drop"/); 903 | }); 904 | 905 | it('converts to wall seconds correctly', () => { 906 | expect(Code.WALL_SECS_TO_TC_RIGHT(1.04, '50.00', 'non-drop')).toBe('00:00:01:02'); 907 | 908 | expect(Code.WALL_SECS_TO_TC_RIGHT(60.02663333, '29.97', 'drop')).toBe('00:00:59:29'); 909 | expect(Code.WALL_SECS_TO_TC_RIGHT(60.02663334, '29.97', 'drop')).toBe('00:01:00:02'); 910 | 911 | // 44:33:22:11 => 160,402 timecode seconds plus 11 frames: 912 | 913 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.86079166, '23.976', 'non-drop')).toBe('44:33:22:11'); 914 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.86079166, '23.98', 'non-drop')).toBe('44:33:22:11'); 915 | 916 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.45833333, '24.000', 'non-drop')).toBe('44:33:22:11'); 917 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.45833333, '24.00', 'non-drop')).toBe('44:33:22:11'); 918 | 919 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.44, '25.000', 'non-drop')).toBe('44:33:22:11'); 920 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.44, '25.00', 'non-drop')).toBe('44:33:22:11'); 921 | 922 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.76903333, '29.970', 'non-drop')).toBe('44:33:22:11'); 923 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.76903333, '29.97', 'non-drop')).toBe('44:33:22:11'); 924 | 925 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.36666666, '30.000', 'non-drop')).toBe('44:33:22:11'); 926 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.36666666, '30.00', 'non-drop')).toBe('44:33:22:11'); 927 | 928 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.63139583, '47.952', 'non-drop')).toBe('44:33:22:11'); 929 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.63139583, '47.95', 'non-drop')).toBe('44:33:22:11'); 930 | 931 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.22916666, '48.000', 'non-drop')).toBe('44:33:22:11'); 932 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.22916666, '48.00', 'non-drop')).toBe('44:33:22:11'); 933 | 934 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.22, '50.000', 'non-drop')).toBe('44:33:22:11'); 935 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.22, '50.00', 'non-drop')).toBe('44:33:22:11'); 936 | 937 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.58551666, '59.940', 'non-drop')).toBe('44:33:22:11'); 938 | expect(Code.WALL_SECS_TO_TC_RIGHT(160562.58551666, '59.94', 'non-drop')).toBe('44:33:22:11'); 939 | 940 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.18333333, '60.000', 'non-drop')).toBe('44:33:22:11'); 941 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.18333333, '60.00', 'non-drop')).toBe('44:33:22:11'); 942 | 943 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.20863333, '29.970', 'drop')).toBe('44:33:22:11'); 944 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.20863333, '29.97', 'drop')).toBe('44:33:22:11'); 945 | 946 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.02511666, '59.940', 'drop')).toBe('44:33:22:11'); 947 | expect(Code.WALL_SECS_TO_TC_RIGHT(160402.02511666, '59.94', 'drop')).toBe('44:33:22:11'); 948 | }); 949 | }); 950 | 951 | describe('FRAMEIDX_TO_TC', () => { 952 | it('rejects invalid frameIdx values and timecode standards', () => { 953 | expect(() => Code.FRAMEIDX_TO_TC(-1234, '60.000', 'non-drop')) 954 | .toThrow(/negative timecode values are not supported/); 955 | 956 | expect(() => Code.FRAMEIDX_TO_TC(1234, '12.00', 'non-drop')) 957 | .toThrow(/Unsupported frame rate: "12.00"/); 958 | expect(() => Code.FRAMEIDX_TO_TC(1234, '25.000', 'nondrop')) 959 | .toThrow(/dropType value must be "non-drop" or "drop"/); 960 | }); 961 | 962 | it('converts non-drop frames correctly', () => { 963 | expect(Code.FRAMEIDX_TO_TC(0, '24.00', 'non-drop')).toBe('00:00:00:00'); 964 | expect(Code.FRAMEIDX_TO_TC(1, '29.97', 'non-drop')).toBe('00:00:00:01'); 965 | expect(Code.FRAMEIDX_TO_TC(2, '50.000', 'non-drop')).toBe('00:00:00:02'); 966 | 967 | expect(Code.FRAMEIDX_TO_TC(24, '24.00', 'non-drop')).toBe('00:00:01:00'); 968 | expect(Code.FRAMEIDX_TO_TC(31, '29.97', 'non-drop')).toBe('00:00:01:01'); 969 | expect(Code.FRAMEIDX_TO_TC(52, '50.000', 'non-drop')).toBe('00:00:01:02'); 970 | 971 | // 160,402 timecode seconds plus 11 frames: 972 | expect(Code.FRAMEIDX_TO_TC(3849659, '23.976', 'non-drop')).toBe('44:33:22:11'); 973 | expect(Code.FRAMEIDX_TO_TC(3849659, '23.98', 'non-drop')).toBe('44:33:22:11'); 974 | expect(Code.FRAMEIDX_TO_TC(3849659, '24.000', 'non-drop')).toBe('44:33:22:11'); 975 | expect(Code.FRAMEIDX_TO_TC(3849659, '24.00', 'non-drop')).toBe('44:33:22:11'); 976 | 977 | expect(Code.FRAMEIDX_TO_TC(4010061, '25.000', 'non-drop')).toBe('44:33:22:11'); 978 | expect(Code.FRAMEIDX_TO_TC(4010061, '25.00', 'non-drop')).toBe('44:33:22:11'); 979 | 980 | expect(Code.FRAMEIDX_TO_TC(4812071, '29.970', 'non-drop')).toBe('44:33:22:11'); 981 | expect(Code.FRAMEIDX_TO_TC(4812071, '29.97', 'non-drop')).toBe('44:33:22:11'); 982 | expect(Code.FRAMEIDX_TO_TC(4812071, '30.000', 'non-drop')).toBe('44:33:22:11'); 983 | expect(Code.FRAMEIDX_TO_TC(4812071, '30.00', 'non-drop')).toBe('44:33:22:11'); 984 | 985 | expect(Code.FRAMEIDX_TO_TC(7699307, '47.952', 'non-drop')).toBe('44:33:22:11'); 986 | expect(Code.FRAMEIDX_TO_TC(7699307, '47.95', 'non-drop')).toBe('44:33:22:11'); 987 | expect(Code.FRAMEIDX_TO_TC(7699307, '48.000', 'non-drop')).toBe('44:33:22:11'); 988 | expect(Code.FRAMEIDX_TO_TC(7699307, '48.00', 'non-drop')).toBe('44:33:22:11'); 989 | 990 | expect(Code.FRAMEIDX_TO_TC(8020111, '50.000', 'non-drop')).toBe('44:33:22:11'); 991 | expect(Code.FRAMEIDX_TO_TC(8020111, '50.00', 'non-drop')).toBe('44:33:22:11'); 992 | 993 | expect(Code.FRAMEIDX_TO_TC(9624131, '59.940', 'non-drop')).toBe('44:33:22:11'); 994 | expect(Code.FRAMEIDX_TO_TC(9624131, '59.94', 'non-drop')).toBe('44:33:22:11'); 995 | expect(Code.FRAMEIDX_TO_TC(9624131, '60.000', 'non-drop')).toBe('44:33:22:11'); 996 | expect(Code.FRAMEIDX_TO_TC(9624131, '60.00', 'non-drop')).toBe('44:33:22:11'); 997 | }); 998 | 999 | it('converts drop frames correctly', () => { 1000 | expect(Code.FRAMEIDX_TO_TC(1799, '29.97', 'drop')).toBe('00:00:59:29'); 1001 | expect(Code.FRAMEIDX_TO_TC(1800, '29.97', 'drop')).toBe('00:01:00:02'); 1002 | 1003 | // 6*44 + 3 = 267 blocks of 10 minutes, plus 00:03:22:11: 1004 | 1005 | // 29.97 drop: 17,982 frames per 10 minutes (267 * 17,982 = 4,801,194), 1006 | // plus 00:03:22:11 (202s*30/s + 11 = 6,071 frames; minus 3m * 2/m = 6 dropped), 1007 | // total 4,801,194 + 6,071 - 6 = 4,807,259. 1008 | expect(Code.FRAMEIDX_TO_TC(4807259, '29.970', 'drop')).toBe('44:33:22:11'); 1009 | 1010 | // 59.94 drop: 35,964 frames per 10 minutes (267 * 35,964 = 9,602,388), 1011 | // plus 00:03:22:11 (202s*60/s + 11 = 12,131 frames; minus 3m * 4/m = 12 dropped), 1012 | // total 9,602,388 + 12,131 - 12 = 9,614,507. 1013 | expect(Code.FRAMEIDX_TO_TC(9614507, '59.940', 'drop')).toBe('44:33:22:11'); 1014 | }); 1015 | }); 1016 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Barndollar Music, Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gsheets-timecode 2 | [![Build Status](https://travis-ci.com/barndollarmusic/gsheets-timecode.svg?branch=main)](https://travis-ci.com/barndollarmusic/gsheets-timecode) 3 | 4 | Google Sheets custom functions for working with video timecode standards and wall time durations. 5 | 6 | ![Preview of spreadsheet using timecode functions](preview.png) 7 | 8 | *Using Microsoft Excel instead? Check out [excel-timecode](https://github.com/barndollarmusic/excel-timecode)*. 9 | 10 | Designed for film & television composers, though these may be useful for anyone who works with 11 | timecode values in Google Sheets. 12 | 13 | Primary Author: [Eric Barndollar](https://barndollarmusic.com) 14 | 15 | This is open source software that is free to use and share, as covered by the 16 | [MIT License](LICENSE). 17 | 18 | # Use in your own spreadsheets 19 | The easiest way to use these functions is by making a copy of this example spreadsheet template: 20 | - [Music Log Template](https://docs.google.com/spreadsheets/d/1xPi0lxi4-4NmZmNoTXXoCNa0FGIAhwi2QCPjTABJCw4/edit?usp=sharing) 21 | 22 | Or if you want to start from your own existing spreadsheet, go to **Tools > Script editor** and 23 | paste [this code](Code.js) into `Code.gs`. 24 | 25 | # Using custom functions 26 | Here's a spreadsheet that will walk you through how to use all of these functions: 27 | - [Tutorial: gsheets-timecode](https://docs.google.com/spreadsheets/d/1QephM04_TBnmzdKqT3WXLeGo0JNX1N1SlD3_BdpfY0E/edit?usp=sharing) 28 | 29 | The last 2 arguments to every function below are `frameRate` and `dropType` values. 30 | 31 | Data validation list of supported `frameRate` values (see template above for example usage): 32 | ``` 33 | 23.976,24.000,25.000,29.970,30.000,47.952,48.000,50.000,59.940,60.000 34 | ``` 35 | **IMPORTANT**: The `frameRate` value must be **Plain text** type (not a number) and include exactly 36 | 2 or 3 decimal digits after a period. This is to avoid any possible confusion over *e.g.* whether 37 | `24` means `23.976` or `24.000`. 38 | 39 | Data validation list of `dropType` values (see template above for example usage): 40 | ``` 41 | non-drop,drop 42 | ``` 43 | 44 | ## Most common functions 45 | All the examples below show timecode values as *Plain text* (quoted string), but you can instead use an 46 | integer *Number* format input (which can be more convenient to type in, along with a custom number format 47 | of `00\:00\:00\:00`). 48 | 49 | ```JavaScript 50 | =TC_TO_WALL_SECS("00:00:01:02", "50.00", "non-drop") 51 | ``` 52 | - Yields `1.04` secs (true seconds of wall time measured from `00:00:00:00`). 53 | 54 | ```JavaScript 55 | =WALL_SECS_BETWEEN_TCS("00:00:01:03", "00:02:05:11", "24.00", "non-drop") 56 | ``` 57 | - Yields `124.33333333...` secs (true seconds of wall time between the timecodes). 58 | 59 | ```JavaScript 60 | =WALL_SECS_TO_DURSTR(3765) 61 | ``` 62 | - Yields `"1h 02m 45s"` (a human-readable duration string). Rounds to nearest second. 63 | 64 | ```JavaScript 65 | =WALL_SECS_TO_TC_LEFT(1.041, "50.00", "non-drop") 66 | ``` 67 | - Yields `"00:00:01:02"`, the timecode of the closest frame that is exactly at or 68 | before (*i.e.* to the left of) the given `wallSecs` value of `1.041` (true seconds of 69 | wall time measured from `00:00:00:00`). 70 | 71 | ```JavaScript 72 | =WALL_SECS_TO_TC_RIGHT(1.041, "50.00", "non-drop") 73 | ``` 74 | - Yields `"00:00:01:03"`, the timecode of the closest frame that is exactly at or 75 | after (*i.e.* to the right of) the given `wallSecs` value of `1.041` (true seconds of 76 | wall time measured from `00:00:00:00`). 77 | 78 | ## Other functions (more advanced) 79 | ```JavaScript 80 | =TC_ERROR("01:02:03:04", "23.976", "non-drop") 81 | ``` 82 | - Yields an error string if timecode (or format) is invalid, or an empty string otherwise. 83 | 84 | ```JavaScript 85 | =TC_TO_FRAMEIDX("00:00:01:02", "50.00", "non-drop") 86 | ``` 87 | - Yields `52` (the timecode refers to the 53rd frame of video, counting from `00:00:00:00` as 88 | index 0). Dropped frames are not given index values (so in 29.97 drop, `00:00:59:29` has index 89 | `1799` and `00:01:00:02` has index `1800`). 90 | 91 | ```JavaScript 92 | =FRAMEIDX_TO_TC(52, "50.00", "non-drop") 93 | ``` 94 | - Yields `"00:00:01:02"`, the timecode of the given frame index. 95 | 96 | ```JavaScript 97 | =FRAMEIDX_TO_WALL_SECS(52, "50.00", "non-drop") 98 | ``` 99 | - Yields `1.04` secs (true seconds of wall time measured from `00:00:00:00`). 100 | 101 | ```JavaScript 102 | =WALL_SECS_TO_FRAMEIDX_LEFT(1.041, "50.00", "non-drop") 103 | ``` 104 | - Yields `52`, the frame index of the closest frame that is exactly at or 105 | before (*i.e.* to the left of) the given `wallSecs` value of `1.041` (true seconds of 106 | wall time measured from `00:00:00:00`). 107 | 108 | ```JavaScript 109 | =WALL_SECS_TO_FRAMEIDX_RIGHT(1.041, "50.00", "non-drop") 110 | ``` 111 | - Yields `53`, the frame index of the closest frame that is exactly at or 112 | after (*i.e.* to the right of) the given `wallSecs` value of `1.041` (true seconds of 113 | wall time measured from `00:00:00:00`). 114 | 115 | # Acknowledgements & Other Resources 116 | 117 | Special thanks to [Eduardo Delgado](https://sonicscapeproductions.com/) for suggesting improvements 118 | and helping with the [Excel version](https://github.com/barndollarmusic/excel-timecode). 119 | 120 | Find the link to Shie Rozow's **SR Show Cue Manager** Google Sheet template along with excellent 121 | advice for collaborative project management and organization: 122 | *[Scoring Films on a Shoestring Budget](https://shierozow.com/scoring-films-on-a-shoestring-budget/)* 123 | 124 | Tim Starnes also has a great *[File Naming and Organization](https://youtu.be/z88kv81yKTk)* video on 125 | the Cinesamples YouTube channel. 126 | 127 | # Contributing Code 128 | Please add tests for any changes to [Code.test.js](Code.test.js), and run all tests on the 129 | command-line with: 130 | 131 | ``` 132 | npm test 133 | ``` 134 | -------------------------------------------------------------------------------- /appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "America/New_York", 3 | "dependencies": { 4 | }, 5 | "exceptionLogging": "STACKDRIVER", 6 | "runtimeVersion": "V8" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gsheets-timecode", 3 | "version": "1.0.0", 4 | "description": "Google Sheets custom functions for working with video timecode standards and wall time durations", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/barndollarmusic/gsheets-timecode.git" 12 | }, 13 | "author": "Eric Barndollar", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/barndollarmusic/gsheets-timecode/issues" 17 | }, 18 | "homepage": "https://github.com/barndollarmusic/gsheets-timecode#readme", 19 | "devDependencies": { 20 | "@google/clasp": "^2.4.2", 21 | "jest": "^29.2.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barndollarmusic/gsheets-timecode/2cb9c3ba1caf47818c11e564a1cca8f148e081f2/preview.png --------------------------------------------------------------------------------