From b8793b8b6569ceb33ed59e3921f3a807e7d917b6 Mon Sep 17 00:00:00 2001 From: Tyler Camp Date: Fri, 7 Sep 2018 20:26:53 -0400 Subject: [PATCH] Reports upload ignores reports over 2 weeks old, add ManyTasks utility class - optimization for making many queries at once, bugfix for map script crashing if no incoming data is available for a hovered village --- TW.Vault/Controllers/AdminController.cs | 36 ++--- TW.Vault/Controllers/CommandController.cs | 46 +++---- TW.Vault/Controllers/VillageController.cs | 46 ++++--- TW.Vault/ManyTasks.cs | 64 +++++++++ TW.Vault/wwwroot/lib/lib.js | 91 ++++++++---- TW.Vault/wwwroot/pages/reports-overview.js | 16 +++ TW.Vault/wwwroot/ui/map.js | 8 +- TW.Vault/wwwroot/unit-tests.js | 152 +++++++++++++++++++++ 8 files changed, 365 insertions(+), 94 deletions(-) create mode 100644 TW.Vault/ManyTasks.cs create mode 100644 TW.Vault/wwwroot/unit-tests.js diff --git a/TW.Vault/Controllers/AdminController.cs b/TW.Vault/Controllers/AdminController.cs index 4662ca8..7a9b673 100644 --- a/TW.Vault/Controllers/AdminController.cs +++ b/TW.Vault/Controllers/AdminController.cs @@ -272,24 +272,24 @@ public async Task GetTroopsSummary() // This is a mess because of different classes for Player, CurrentPlayer, etc - // Get all CurrentVillages from the user's tribe - list of (Player, CurrentVillage) - var tribeVillages = await ( - from player in context.Player.FromWorld(CurrentWorldId) - join village in context.Village.FromWorld(CurrentWorldId) on player.PlayerId equals village.PlayerId - join currentVillage in context.CurrentVillage.IncludeCurrentVillageData() - on village.VillageId equals currentVillage.VillageId - where player.TribeId == CurrentTribeId - select new { player, currentVillage } - ).ToListAsync(); - - // Get all CurrentPlayer data for the user's tribe (separate from global 'Player' table - // so we can also output stats for players that haven't uploaded anything yet) - var currentPlayers = await ( - from currentPlayer in context.CurrentPlayer.FromWorld(CurrentWorldId) - join player in context.Player on currentPlayer.PlayerId equals player.PlayerId - where player.TribeId == CurrentTribeId - select currentPlayer - ).ToListAsync(); + var (tribeVillages, currentPlayers) = await ManyTasks.RunToList( + // Get all CurrentVillages from the user's tribe - list of (Player, CurrentVillage) + from player in context.Player.FromWorld(CurrentWorldId) + join village in context.Village.FromWorld(CurrentWorldId) on player.PlayerId equals village.PlayerId + join currentVillage in context.CurrentVillage.IncludeCurrentVillageData() + on village.VillageId equals currentVillage.VillageId + where player.TribeId == CurrentTribeId + select new { player, currentVillage } + + , + + // Get all CurrentPlayer data for the user's tribe (separate from global 'Player' table + // so we can also output stats for players that haven't uploaded anything yet) + from currentPlayer in context.CurrentPlayer.FromWorld(CurrentWorldId) + join player in context.Player on currentPlayer.PlayerId equals player.PlayerId + where player.TribeId == CurrentTribeId + select currentPlayer + ); // Collect villages grouped by owner var villagesByPlayer = tribeVillages diff --git a/TW.Vault/Controllers/CommandController.cs b/TW.Vault/Controllers/CommandController.cs index 31e42e2..7084ea7 100644 --- a/TW.Vault/Controllers/CommandController.cs +++ b/TW.Vault/Controllers/CommandController.cs @@ -109,15 +109,11 @@ public async Task Post([FromBody]JSON.ManyCommands jsonCommands) var mappedCommands = jsonCommands.Commands.ToDictionary(c => c.CommandId, c => c); var commandIds = jsonCommands.Commands.Select(c => c.CommandId).ToList(); - - var scaffoldCommands = await Profile("Get existing scaffold commands", () => ( - from command in context.Command.IncludeCommandData().FromWorld(CurrentWorldId) - where commandIds.Contains(command.CommandId) - select command - ).ToListAsync() - ); - - var mappedScaffoldCommands = scaffoldCommands.ToDictionary(c => c.CommandId, c => c); + var allVillageIds = jsonCommands.Commands + .Select(c => c.SourceVillageId) + .Concat(jsonCommands.Commands.Select(c => c.TargetVillageId)) + .Select(id => id.Value) + .Distinct(); var villageIdsFromCommandsMissingTroopType = jsonCommands.Commands .Where(c => c.TroopType == null) @@ -125,24 +121,26 @@ select command .Distinct() .ToList(); - var villagesForMissingTroopTypes = await ( - from village in context.Village.FromWorld(CurrentWorldId) - where villageIdsFromCommandsMissingTroopType.Contains(village.VillageId) - select village - ).ToListAsync(); + var (scaffoldCommands, villageIdsFromCommandsMissingTroopTypes, allVillages) = await ManyTasks.RunToList( + from command in context.Command.IncludeCommandData().FromWorld(CurrentWorldId) + where commandIds.Contains(command.CommandId) + select command + + , + + from village in context.Village.FromWorld(CurrentWorldId) + where villageIdsFromCommandsMissingTroopType.Contains(village.VillageId) + select village - var allVillageIds = jsonCommands.Commands - .Select(c => c.SourceVillageId) - .Concat(jsonCommands.Commands.Select(c => c.TargetVillageId)) - .Select(id => id.Value) - .Distinct(); + , - var allVillages = await ( - from village in context.Village.FromWorld(CurrentWorldId) - where allVillageIds.Contains(village.VillageId) - select village - ).ToListAsync(); + from village in context.Village.FromWorld(CurrentWorldId) + where allVillageIds.Contains(village.VillageId) + select village + ); + + var mappedScaffoldCommands = scaffoldCommands.ToDictionary(c => c.CommandId, c => c); var villagesById = allVillages.ToDictionary(v => v.VillageId, v => v); var tx = BuildTransaction(); diff --git a/TW.Vault/Controllers/VillageController.cs b/TW.Vault/Controllers/VillageController.cs index bd9c949..7793748 100644 --- a/TW.Vault/Controllers/VillageController.cs +++ b/TW.Vault/Controllers/VillageController.cs @@ -109,20 +109,21 @@ select tx } // Start getting village data - var currentVillage = await Profile("Get current village", () => ( + + var (currentVillage, commandsToVillage) = await ManyTasks.Run( + Profile("Get current village", () => ( from cv in context.CurrentVillage - .FromWorld(CurrentWorldId) - .Include(v => v.ArmyOwned) - .Include(v => v.ArmyStationed) - .Include(v => v.ArmyTraveling) - .Include(v => v.ArmyRecentLosses) - .Include(v => v.CurrentBuilding) + .FromWorld(CurrentWorldId) + .Include(v => v.ArmyOwned) + .Include(v => v.ArmyStationed) + .Include(v => v.ArmyTraveling) + .Include(v => v.ArmyRecentLosses) + .Include(v => v.CurrentBuilding) where cv.VillageId == villageId select cv - ).FirstOrDefaultAsync() - ); + ).FirstOrDefaultAsync()), - var commandsToVillage = await Profile("Get commands to village", () => ( + Profile("Get commands to village", () => ( from command in context.Command .FromWorld(CurrentWorldId) .Include(c => c.Army) @@ -130,9 +131,10 @@ from command in context.Command where !command.IsReturning where command.LandsAt > CurrentServerTime select command - ).ToListAsync() + ).ToListAsync()) ); + var jsonData = new JSON.VillageData(); // Return empty data if no data is available for the village @@ -274,13 +276,22 @@ public async Task PostCurrentArmy([FromBody]JSON.PlayerArmy curre var villageIds = currentArmySetJson.TroopData.Select(a => a.VillageId.Value).ToList(); - var scaffoldCurrentVillages = await Profile("Get existing scaffold current villages", () => ( + var (scaffoldCurrentVillages, villagesWithPlayerIds) = await ManyTasks.Run( + Profile("Get existing scaffold current villages", () => ( from cv in context.CurrentVillage.FromWorld(CurrentWorldId).IncludeCurrentVillageData() where villageIds.Contains(cv.VillageId) select cv - ).ToListAsync() + ).ToListAsync()) + , + Profile("Get village player IDs", () => ( + from v in context.Village.FromWorld(CurrentWorldId) + where villageIds.Contains(v.VillageId) + select new { v.PlayerId, v.VillageId } + ).ToListAsync()) ); + var villageIdsByPlayerId = villagesWithPlayerIds.ToDictionary(v => v.VillageId, v => v.PlayerId); + var mappedScaffoldVillages = villageIds.ToDictionary(id => id, id => scaffoldCurrentVillages.SingleOrDefault(cv => cv.VillageId == id)); var missingScaffoldVillageIds = mappedScaffoldVillages.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key).ToList(); @@ -293,13 +304,6 @@ select v ).ToListAsync() ); - var villagePlayerIds = (await Profile("Get village player IDs", () => ( - from v in context.Village.FromWorld(CurrentWorldId) - where villageIds.Contains(v.VillageId) - select new { v.PlayerId, v.VillageId } - ).ToListAsync() - )).ToDictionary(v => v.VillageId, v => v.PlayerId); - var mappedMissingVillageData = missingVillageData.ToDictionary(vd => vd.VillageId, vd => vd); // Get or make CurrentVillage @@ -324,7 +328,7 @@ where villageIds.Contains(v.VillageId) foreach (var armySetJson in currentArmySetJson.TroopData) { var currentVillage = mappedScaffoldVillages[armySetJson.VillageId.Value]; - var villagePlayerId = villagePlayerIds[currentVillage.VillageId]; + var villagePlayerId = villageIdsByPlayerId[currentVillage.VillageId]; if (!Configuration.Security.AllowUploadArmyForNonOwner && villagePlayerId != CurrentUser.PlayerId) diff --git a/TW.Vault/ManyTasks.cs b/TW.Vault/ManyTasks.cs new file mode 100644 index 0000000..91fa22c --- /dev/null +++ b/TW.Vault/ManyTasks.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace TW.Vault +{ + public static class ManyTasks + { + public static async Task> Run(Task task1) + { + return new Tuple(await task1); + } + + public static async Task> Run(Task task1, Task task2) + { + var tasks = new Task[] { task1, task2 }; + await Task.WhenAll(tasks); + return new Tuple(task1.Result, task2.Result); + } + + public static async Task> Run(Task task1, Task task2, Task task3) + { + var tasks = new Task[] { task1, task2, task3 }; + await Task.WhenAll(tasks); + return new Tuple(task1.Result, task2.Result, task3.Result); + } + + public static async Task> Run(Task task1, Task task2, Task task3, Task task4) + { + var tasks = new Task[] { task1, task2, task3, task4 }; + await Task.WhenAll(tasks); + return new Tuple(task1.Result, task2.Result, task3.Result, task4.Result); + } + + public static async Task> Run(Task task1, Task task2, Task task3, Task task4, Task task5) + { + var tasks = new Task[] { task1, task2, task3, task4, task5 }; + await Task.WhenAll(tasks); + return new Tuple(task1.Result, task2.Result, task3.Result, task4.Result, task5.Result); + } + + public static async Task> Run(Task task1, Task task2, Task task3, Task task4, Task task5, Task task6) + { + var tasks = new Task[] { task1, task2, task3, task4, task5, task6 }; + await Task.WhenAll(tasks); + return new Tuple(task1.Result, task2.Result, task3.Result, task4.Result, task5.Result, task6.Result); + } + + + + public static Task>> RunToList(IQueryable q1) => + Run(q1.ToListAsync()); + + public static Task, List>> RunToList(IQueryable q1, IQueryable q2) => + Run(q1.ToListAsync(), q2.ToListAsync()); + + public static Task, List, List>> RunToList(IQueryable q1, IQueryable q2, IQueryable q3) => + Run(q1.ToListAsync(), q2.ToListAsync(), q3.ToListAsync()); + } +} diff --git a/TW.Vault/wwwroot/lib/lib.js b/TW.Vault/wwwroot/lib/lib.js index 5a9e2ad..d837a13 100644 --- a/TW.Vault/wwwroot/lib/lib.js +++ b/TW.Vault/wwwroot/lib/lib.js @@ -53,19 +53,24 @@ var lib = (() => { twcalc: makeTwCalc(twstats), // Gets the current server date and time from the page - getServerDateTime: function getServerDateTime() { + getServerDateTime: function getServerDateTime($doc_) { + $doc_ = $doc_ || $(document); var $serverDate = $('#serverDate'); var $serverTime = $('#serverTime'); - throw "Not yet implemented"; + + let fullString = `${$serverTime.text().trim()} ${$serverDate.text().trim()}`; + return lib.parseTimeString(fullString); }, // Parses a variety of TW date/time formats to JS Date or into splits it into its parts // returns Date if separated_ false or undefined // returns { date: [day, month, year], time: [hour, minute, second, millisecond] } if separated_ is true - parseTimeString: function parseTimeString(timeString, separated_) { + parseTimeString: function parseTimeString(timeString, separated_, $doc_) { if (typeof separated_ == 'undefined') separated_ = false; + $doc_ = $doc_ || $(document); + var monthStrings = [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' @@ -73,66 +78,70 @@ var lib = (() => { var result; + let dateSeparators = ['/', '.']; + let timeSeparators = [':', '.']; + let dateSeparatorsStr = dateSeparators.map((s) => `\\${s}`).join(''); + let timeSeparatorsStr = timeSeparators.map((s) => `\\${s}`).join(''); + let dateRegex = `(\\d+[${dateSeparatorsStr}]\\d+(?:[${dateSeparatorsStr}]\\d+)?)\\.?`; + let timeRegex = `(\\d+[${timeSeparatorsStr}]\\d+(?:[${timeSeparatorsStr}]\\d+)?(?:[${timeSeparatorsStr}]\\d+)?)`; + + var serverDate = $doc_.find('#serverDate').text().split('/'); + var match; - if (match = timeString.match(/(\d+:\d+:\d+:\d+)\s+(\d+\/\d+\/\d+)/)) { + if (match = timeString.match(new RegExp(`${timeRegex} ${dateRegex}`))) { // Hour:Minute:Second:Ms Day/Month/Year result = { - time: match[1].split(':'), - date: match[2].split('/') + time: match[1].splitMany(timeSeparators), + date: match[2].splitMany(dateSeparators) }; - } else if (match = timeString.match(/(\d+\/\d+\/\d+)\s+(\d+:\d+:\d+:\d+)/)) { + } else if (match = timeString.match(new RegExp(`${dateRegex} ${timeRegex}`))) { // Day/Month/Year Hour:Minute:Second:Ms result = { - date: match[1].split('/'), - time: match[2].split(':') + date: match[1].splitMany(dateSeparators), + time: match[2].splitMany(timeSeparators) }; - } else if (match = timeString.match(new RegExp(`((?:${monthStrings.join('|')}))\\s+(\\d+),\\s+(\\d+)\\s+(\\d+:\\d+:\\d+:\\d+)`, 'i'))) { + } else if (match = timeString.match(new RegExp(`((?:${monthStrings.join('|')}))\\.?\\s+(\\d+),\\s+(?:(\\d+)\\s+)?${timeRegex}`, 'i'))) { // (Mon.) Day, Year Hour:Minute:Second:Ms var monthName = match[1]; var day = match[2]; - var year = match[3]; + var year = match[3] || serverDate[2]; var month = (monthStrings.indexOf(monthName.toLowerCase()) + 1).toString(); result = { date: [day, month, year], - time: match[4].split(':') + time: match[4].splitMany(timeSeparators) }; - } else if (match = timeString.match(/today at\s+(\d+:\d+:\d+:\d+)/)) { + } else if (match = timeString.match(new RegExp(`today at\\s+(${timeRegex})`))) { // today at (Hours:Minute:Second:Ms) - var serverDate = $('#serverDate').text().split('/'); result = { date: serverDate, - time: match[1].split(':') + time: match[1].splitMany(timeSeparators) } - } else if (match = timeString.match(/tomorrow at\s+(\d+:\d+:\d+:\d+)/)) { + } else if (match = timeString.match(new RegExp(`tomorrow at\\s+(${timeRegex})`))) { // tomorrow at (Hours:Minute:Second:Ms) - var serverDate = $('#serverDate').text().split('/'); - result = { date: [ (parseInt(serverDate[0]) + 1).toString(), (parseInt(serverDate[1])).toString(), serverDate[2] ], - time: match[1].split(':') + time: match[1].splitMany(timeSeparators) }; + // TODO - Update this one } else if (match = timeString.match(/on (\d+[\/\.]\d+(?:[\/\.](?:\d+)?)?)\s+at\s+(\d+:\d+:\d+:\d+)/)) { // on (Day/Month/Year) at (Hours:Minute:Second:Ms) result = { - date: match[1].contains('/') ? match[1].split('/') : match[1].split('.'), - time: match[2].split(':') + date: match[1].splitMany(dateSeparators), + time: match[2].splitMany(timeSeparators) }; - - if (result.date.length < 2) { - result.date.push(null); - } + if (!result.date[2]) { - result.date[2] = $('#serverDate').text().split('/')[2]; + result.date[2] = serverDate[2]; } } else { @@ -146,6 +155,10 @@ var lib = (() => { result.date.forEach((val, i) => result.date[i] = parseInt(val)); result.time.forEach((val, i) => result.time[i] = parseInt(val)); + result.date[2] = result.date[2] || parseInt(serverDate[2]); + result.time[2] = result.time[2] || 0; + result.time[3] = result.time[3] || 0; + if (separated_) { return result; } else { @@ -333,7 +346,7 @@ var lib = (() => { url = '/game.php?' + url; - var t = query[t]; + var t = query['t']; if (t) { t = t.match(/t=(\w+)/)[1]; if (url.contains("?")) { @@ -689,6 +702,30 @@ var lib = (() => { return this.indexOf(str) >= 0; }; + String.prototype.splitMany = function splitMany() { + var splitTokens = []; + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] == 'string') + splitTokens.push(arguments[i]); + else if (arguments[i] instanceof Array) + splitTokens.push(...arguments[i]); + } + + let result = []; + let workingStr = ''; + for (var i = 0; i < this.length; i++) { + if (splitTokens.contains(this[i])) { + result.push(workingStr); + workingStr = ''; + } else { + workingStr += this[i]; + } + } + result.push(workingStr); + return result; + }; + + // Allow syntax "a,b.c".split('.', ',') -> ['a', 'b', 'c'] let stringOriginalTrim = String.prototype.trim; String.prototype.trim = function trimAll() { var result = stringOriginalTrim.apply(this); diff --git a/TW.Vault/wwwroot/pages/reports-overview.js b/TW.Vault/wwwroot/pages/reports-overview.js index 5061b43..c87a3f9 100644 --- a/TW.Vault/wwwroot/pages/reports-overview.js +++ b/TW.Vault/wwwroot/pages/reports-overview.js @@ -13,15 +13,29 @@ function parseReportsOverviewPage($doc) { console.log('pages = ', pages); let reportLinks = []; + let ignoredReports = []; + let serverTime = lib.getServerDateTime(); + const maxReportAgeDays = 14; + const maxReportAgeMs = maxReportAgeDays * 24 * 60 * 60 * 1000; let $reportLinks = $doc.find('#report_list tr:not(:first-child):not(:last-child) a:not(.rename-icon)'); $reportLinks.each((i, el) => { let $el = $(el); let link = $el.prop('href'); + let landingTimeText = $el.closest('tr').find('td:last-of-type').text(); + let landingTime = lib.parseTimeString(landingTimeText); let reportId = parseInt(link.match(/view=(\w+)/)[1]); let $icon = $el.closest('td').find('img:first-of-type'); + let timeSinceReport = serverTime.valueOf() - landingTime.valueOf(); + if (timeSinceReport >= maxReportAgeMs) { + let ageDays = timeSinceReport / 24 / 60 / 60 / 1000; + ignoredReports.push({ reportId: reportId, ageDays: Math.roundTo(ageDays, 2) }); + //console.log(`Report ${reportId} is ${Math.roundTo(ageDays, 2)} days old, skipping`); + return; + } + var isBattleReport = false; $icon.each((_, el) => { let icon = $(el).attr('src'); @@ -41,5 +55,7 @@ function parseReportsOverviewPage($doc) { }); }); + console.log('Ignored ' + ignoredReports.length + ' reports for being over ' + maxReportAgeDays + ' days old'); + return reportLinks; }; \ No newline at end of file diff --git a/TW.Vault/wwwroot/ui/map.js b/TW.Vault/wwwroot/ui/map.js index c082ea1..938028f 100644 --- a/TW.Vault/wwwroot/ui/map.js +++ b/TW.Vault/wwwroot/ui/map.js @@ -166,11 +166,11 @@ $villageInfoContainer.appendTo($popup); // Update data with what's been loaded by TW (in case someone forgot to upload commands) - let hasRecord = (id) => data.fakes.contains(id) || data.dVs[id] || data.nukes.contains(id); + let hasRecord = (id) => (data.fakes && data.fakes.contains(id)) || (data.dVs && data.dVs[id]) || (data.nukes && data.nukes.contains(id)); - let numFakes = data.fakes.length; - let numNukes = data.nukes.length; - let numPlayers = data.players.length; + let numFakes = data.fakes ? data.fakes.length : 0; + let numNukes = data.nukes ? data.nukes.length : 0; + let numPlayers = data.players ? data.players.length : 0; let numDVs = 0; lib.objForEach(data.Dvs, (commandId, pop) => { diff --git a/TW.Vault/wwwroot/unit-tests.js b/TW.Vault/wwwroot/unit-tests.js new file mode 100644 index 0000000..e6613be --- /dev/null +++ b/TW.Vault/wwwroot/unit-tests.js @@ -0,0 +1,152 @@ +(() => { + //# REQUIRE lib/lib.js + + let scopes = []; + let numPassed = 0, numFailed = 0; + console.log('Starting unit tests.'); + + let $testDoc = $(` +
+
1/11/2018
+
0:0:0
+
+ `.trim()); + + test("Date parsing", () => { + // JS dates have months - 1, so 10 = Nov. + let fullDate = new Date(Date.UTC(2018, 10, 1, 1, 13, 54, 233)); + let dateNoMs = new Date(Date.UTC(2018, 10, 1, 1, 13, 54, 0)); + let dateNoSecs = new Date(Date.UTC(2018, 10, 1, 1, 13, 0, 0)); + let tomorrowFullDate = new Date(Date.UTC(2018, 10, 2, 1, 13, 54, 233)); + + test("Date Time", () => { + let timeString = "01/11/2018 1:13:54:233"; + let parsed = lib.parseTimeString(timeString); + assertEquals(fullDate, parsed); + }); + + test("Date[Periods] Time", () => { + let timeString = "01.11.2018 1:13:54:233"; + let parsed = lib.parseTimeString(timeString); + assertEquals(fullDate, parsed); + }); + + test("Date[Periods+End] Time", () => { + let timeString = "01.11.2018. 1:13:54:233"; + let parsed = lib.parseTimeString(timeString); + assertEquals(fullDate, parsed); + }); + + test("Mon Day, Time (no sec/ms)", () => { + let timeString = "Nov 1, 1:13"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(dateNoSecs, parsed); + }); + + test("Mon. Day, Time (no sec/ms)", () => { + let timeString = "Nov. 1, 1:13"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(dateNoSecs, parsed); + }); + + test("Time Date", () => { + let timeString = "1:13:54:233 01/11/2018"; + let parsed = lib.parseTimeString(timeString); + assertEquals(fullDate, parsed); + }); + + test("Date Time (no ms)", () => { + let timeString = "01/11/2018 1:13:54"; + let parsed = lib.parseTimeString(timeString); + assertEquals(dateNoMs, parsed); + }); + + test("Date Time (no year)", () => { + let timeString = "01/11 1:13:54:233"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(fullDate, parsed); + }); + + test("Today at Time", () => { + let timeString = "today at 1:13:54:233"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(fullDate, parsed); + }); + + test("Today at Time (Missing ms)", () => { + let timeString = "today at 1:13:54"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(dateNoMs, parsed); + }); + + test("Today at Time (Missing mins/ms)", () => { + let timeString = "today at 1:13"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(dateNoSecs, parsed); + }); + + test("Tomorrow at Time", () => { + let timeString = "tomorrow at 1:13:54:233"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(tomorrowFullDate, parsed); + }); + + test("On Date at Time", () => { + let timeString = "on 1/11/2018 at 1:13:54:233"; + let parsed = lib.parseTimeString(timeString); + assertEquals(fullDate, parsed); + }); + + test("On Date[Periods] at Time", () => { + let timeString = "on 1.11.2018 at 1:13:54:233"; + let parsed = lib.parseTimeString(timeString); + assertEquals(fullDate, parsed); + }); + + test("On Date[Periods] at Time (no year)", () => { + let timeString = "on 1.11 at 1:13:54:233"; + let parsed = lib.parseTimeString(timeString, false, $testDoc); + assertEquals(fullDate, parsed); + }); + }); + + + + + + + + + + + + + function test(name, unitTest) { + scopes.push(name); + unitTest(); + scopes.pop(); + } + + function assertEquals(expected, value) { + let printLabel = scopes.join(' : '); + if (compare(expected, value)) { + console.log(`${printLabel} - Passed`); + } else { + console.error(`${printLabel} - FAILED; Expected `, expected, ' got ', value); + } + + + + + function compare(a, b) { + if ((a == null) != (b == null)) + return false; + if (typeof a != typeof b) + return false; + if (a instanceof Date) + return a.valueOf() == b.valueOf(); + + return a == b; + } + } +})(); \ No newline at end of file