diff --git a/src/Acoustics.Shared/FileDateHelpers.cs b/src/Acoustics.Shared/FileDateHelpers.cs index 24b5d95d9..501b89d53 100644 --- a/src/Acoustics.Shared/FileDateHelpers.cs +++ b/src/Acoustics.Shared/FileDateHelpers.cs @@ -23,6 +23,11 @@ namespace Acoustics.Shared public class FileDateHelpers { + public static readonly DateTimeOffset UnixEpoch = + new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + + private const string AudioMothKey = "AudioMoth"; + private static readonly string[] AcceptedFormatsNoTimeZone = { "yyyyMMdd[-|T|_]HHmmss (if timezone offset hint provided)", @@ -63,6 +68,14 @@ public class FileDateHelpers "HHmm-ddMMyyyy", parseTimeZone: false, Array.Empty()), + + // AudioMoth V1 format + // 5BFA3A06.WAV + new DateVariants( + "^(?[0-9A-F]{8}).WAV$", + parseFormat: AudioMothKey, + parseTimeZone: true, + acceptedFormats: Array.Empty()), }; private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); @@ -155,11 +168,15 @@ public static SortedDictionary FilterObjectsForDates(IEnum /// A sorted dictionary FileInfo objects mapped to parsed dates. public static SortedDictionary FilterDirectoriesForDates(IEnumerable directories, TimeSpan? offsetHint = null) { + if (directories == null) + { + throw new ArgumentNullException(nameof(directories)); + } + var datesAndDirs = new SortedDictionary(); foreach (var dir in directories) { - DateTimeOffset parsedDate; - if (FileNameContainsDateTime(dir.Name, out parsedDate, offsetHint)) + if (FileNameContainsDateTime(dir.Name, out var parsedDate, offsetHint)) { datesAndDirs.Add(parsedDate, dir); } @@ -189,7 +206,7 @@ public static bool FileNameContainsDateTime(string fileName, out DateTimeOffset } } - parsedDate = new DateTimeOffset(); + parsedDate = default; return false; } @@ -200,7 +217,7 @@ private static bool ParseFileDateTimeBase( TimeSpan? offsetHint) { var match = Regex.Match(filename, format.Regex); - fileDate = new DateTimeOffset(); + fileDate = default; var successful = match.Success; if (!successful) @@ -210,66 +227,73 @@ private static bool ParseFileDateTimeBase( var stringDate = match.Groups["date"].Value; - var separator = match.Groups["separator"].Value; - - // Normalize the separator - stringDate = stringDate.Replace(separator, "-"); - - if (format.ParseTimeZone) + if (format.ParseFormat == AudioMothKey) + { + successful = ParseAudioMothV1Date(stringDate, out fileDate); + } + else { - var offsetText = match.Groups["offset"].Value; + var separator = match.Groups["separator"].Value; - if (offsetText.Equals("Z", StringComparison.InvariantCultureIgnoreCase)) - { - var parseFormat = format.ParseFormat.Replace("zzz", "Z"); + // Normalize the separator + stringDate = stringDate.Replace(separator, "-"); - successful = DateTimeOffset.TryParseExact( - stringDate, - parseFormat, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal, - out fileDate); - } - else if (offsetText.Length == 5) + if (format.ParseTimeZone) { - // e.g. +1000 - successful = DateTimeOffset.TryParseExact( - stringDate, - format.ParseFormat, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out fileDate); + var offsetText = match.Groups["offset"].Value; + + if (offsetText.Equals("Z", StringComparison.InvariantCultureIgnoreCase)) + { + var parseFormat = format.ParseFormat.Replace("zzz", "Z"); + + successful = DateTimeOffset.TryParseExact( + stringDate, + parseFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal, + out fileDate); + } + else if (offsetText.Length == 5) + { + // e.g. +1000 + successful = DateTimeOffset.TryParseExact( + stringDate, + format.ParseFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out fileDate); + } + else + { + successful = DateTimeOffset.TryParseExact( + stringDate, + format.ParseFormat.Replace("zzz", "zz"), + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out fileDate); + } } else { - successful = DateTimeOffset.TryParseExact( + successful = DateTime.TryParseExact( stringDate, - format.ParseFormat.Replace("zzz", "zz"), + format.ParseFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, - out fileDate); - } - } - else - { - DateTime dateWithoutTimeZone; - successful = DateTime.TryParseExact( - stringDate, - format.ParseFormat, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out dateWithoutTimeZone); - - if (successful) - { - if (offsetHint == null) - { - Log.Warn($"File date `{stringDate}` is amibiguous. The date is understood but no timezone offset could be found and a timezone offset hint was not provided."); - return false; - } - else + out var dateWithoutTimeZone); + + if (successful) { - fileDate = new DateTimeOffset(dateWithoutTimeZone, offsetHint.Value); + if (offsetHint == null) + { + Log.Warn( + $"File date `{stringDate}` is ambiguous. The date is understood but no timezone offset could be found and a timezone offset hint was not provided."); + return false; + } + else + { + fileDate = new DateTimeOffset(dateWithoutTimeZone, offsetHint.Value); + } } } } @@ -277,6 +301,24 @@ private static bool ParseFileDateTimeBase( return successful; } + private static bool ParseAudioMothV1Date(string text, out DateTimeOffset date) + { + var successful = long.TryParse( + text, + NumberStyles.AllowHexSpecifier, + CultureInfo.InvariantCulture, + out var secondsSinceEpoch); + + if (successful) + { + date = UnixEpoch.AddSeconds(secondsSinceEpoch); + return true; + } + + date = default; + return false; + } + internal class DateVariants { public DateVariants(string regex, string parseFormat, bool parseTimeZone, string[] acceptedFormats) diff --git a/tests/Acoustics.Test/Shared/FileDateHelpersTests.cs b/tests/Acoustics.Test/Shared/FileDateHelpersTests.cs index 42694a530..d73cada4d 100644 --- a/tests/Acoustics.Test/Shared/FileDateHelpersTests.cs +++ b/tests/Acoustics.Test/Shared/FileDateHelpersTests.cs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // // All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). // @@ -92,7 +92,7 @@ public void TestInvalidDateFormats() } } - private Dictionary validFormats = new Dictionary + private readonly Dictionary validFormats = new Dictionary { ["sdncv*_-T&^%34jd_20140301_085031+0630blah_T-suffix.mp3"] = Parse("2014-03-01T08:50:31.000+06:30"), ["sdncv*_-T&^%34jd_20140301_085031-0630blah_T-suffix.mp3"] = Parse("2014-03-01T08:50:31.000-06:30"), @@ -101,6 +101,7 @@ public void TestInvalidDateFormats() ["blah_T-suffix20140301-085031Z:dncv*_-T&^%34jd.ext"] = Parse("2014-03-01T08:50:31.000+00:00"), ["SERF_20130314_000021Z_000.wav"] = Parse("2013-03-14T00:00:21.000+00:00"), ["20150727T133138Z.wav"] = Parse("2015-07-27T13:31:38.000+00:00"), + ["5BFA3A06.WAV"] = Parse("2018-11-25T05:58:30.000+00:00"), }; [TestMethod] @@ -118,7 +119,7 @@ public void TestValidDateFormats() } } - private Dictionary validFormatsWithOffsetHint = new Dictionary + private readonly Dictionary validFormatsWithOffsetHint = new Dictionary { ["sdncv*_-T&^%34jd_20140301_085031blah_T-suffix.mp3"] = Parse("2014-03-01T08:50:31.000+06:30"), ["sdncv*_-T&^%34jd_20140301T085031blah_T-suffix.mp3"] = Parse("2014-03-01T08:50:31.000+06:30"), @@ -159,7 +160,7 @@ public void TestValidDateFormatsWithOffsetHint() @"Y:\2015Sept20\Woondum3\20150917-064553Z.wav", @"Y:\2015Sept20\Woondum3\20150917_133143+1000.wav", @"Y:\2015Sept20\Woondum3\20150917_201733+1000.wav", @"Y:\2015Aug2\GympieNP\20150801_000004+1000.wav", - @"Y:\2015Aug2\GympieNP\20150801-064555.wav", @"Y:\2015Aug2\GympieNP\20150801_133148+1000.wav", @"Y:\2015Aug2\GympieNP\20150801-064555+1000.wav", + @"Y:\2015Aug2\GympieNP\20150801-064555.wav", @"Y:\2015Aug2\GympieNP\20150801_133148+1000.wav", @"Y:\2015Aug2\GympieNP\20150801-064555+1000.wav", @"Y:\2015Aug2\GympieNP\20150801-201742+1000.wav", @"Y:\2015Aug2\GympieNP\20150802-000006Z.wav", @"Y:\2015Aug2\GympieNP\20150802-064559+1000.wav", @"Y:\2015Sept20\Woondum3\20150919_000006+1000.wav", @"Y:\2015Sept20\Woondum3\20150919_064557+1000.wav", @"Y:\2015Sept20\Woondum3\20150919-133149+1000.wav",