package wav
import (
"testing"
"time"
)
type expectedTS struct {
Year, Month, Day, Hour, Minute, Second int
}
func assertTimestamp(t *testing.T, got time.Time, want expectedTS) {
t.Helper()
t.Helper()
if got.Year() != want.Year {
t.Errorf("Year: got %d, want %d", got.Year(), want.Year)
}
if got.Month() != time.Month(want.Month) {
t.Errorf("Month: got %d, want %d", got.Month(), want.Month)
}
if got.Day() != want.Day {
t.Errorf("Day: got %d, want %d", got.Day(), want.Day)
}
if got.Hour() != want.Hour {
t.Errorf("Hour: got %d, want %d", got.Hour(), want.Hour)
}
if got.Minute() != want.Minute {
t.Errorf("Minute: got %d, want %d", got.Minute(), want.Minute)
}
if got.Second() != want.Second {
t.Errorf("Second: got %d, want %d", got.Second(), want.Second)
}
}
func assertOffset(t *testing.T, got time.Time, wantSeconds int) {
t.Helper()
_, offset := got.Zone()
if offset != wantSeconds {
t.Errorf("Offset: got %d seconds, want %d seconds", offset, wantSeconds)
}
}
func parseAndApply(t *testing.T, filenames []string, tz string) []time.Time {
t.Helper()
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, tz)
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
return results
}
type parseTestCase struct {
name string
files []string
expected map[int]expectedTS }
func runParseTestCase(t *testing.T, tc parseTestCase) {
t.Helper()
results, err := ParseFilenameTimestamps(tc.files)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != len(tc.files) {
t.Fatalf("Expected %d results, got %d", len(tc.files), len(results))
}
for idx, want := range tc.expected {
assertTimestamp(t, results[idx].Timestamp, want)
}
}
func TestParseFilenameTimestamps(t *testing.T) {
cases := []parseTestCase{
{
name: "YYMMDD format (test case a)",
files: []string{"201012_123456.wav", "201014_123456.WAV", "201217_123456.wav", "211122_123456.WAV"},
expected: map[int]expectedTS{
0: {2020, 10, 12, 12, 34, 56}, 3: {2021, 11, 22, 12, 34, 56},
},
},
{
name: "DDMMYY format (test case b)",
files: []string{"121020_123456.WAV", "141020_123456.wav", "171220_123456.WAV", "221121_123456.wav"},
expected: map[int]expectedTS{
0: {2020, 10, 12, 12, 34, 56},
2: {2020, 12, 17, 12, 34, 56},
},
},
{
name: "YYYYMMDD format (test case c)",
files: []string{"20230609_103000.WAV", "20241109_201504.wav"},
expected: map[int]expectedTS{
0: {2023, 6, 9, 10, 30, 0},
1: {2024, 11, 9, 20, 15, 4},
},
},
{
name: "6-digit with variance detection (test case d)",
files: []string{"120119_003002.wav", "180120_231502.wav", "170122_010005.wav", "010419_234502.WAV", "310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},
expected: map[int]expectedTS{
0: {2019, 1, 12, 0, 30, 2}, 4: {2020, 3, 31, 23, 15, 2},
},
},
{
name: "prefixes (test case e)",
files: []string{"XYZ123_7689_20230609_103000.WAV", "string 20241109_201504.wav"},
expected: map[int]expectedTS{
0: {2023, 6, 9, 10, 30, 0},
1: {2024, 11, 9, 20, 15, 4},
},
},
{
name: "complex prefixes (test case f)",
files: []string{"abcdefg__1234_180120_231502.wav", "string 120119_003002.wav", "ABCD EFG___170122_010005.wav", "BHD_1234 010419_234502.WAV", "cill xyz 310320_231502.wav", "220824_231502.WAV", "240123_231502.wav"},
expected: map[int]expectedTS{
0: {2020, 1, 18, 23, 15, 2},
1: {2019, 1, 12, 0, 30, 2},
4: {2020, 3, 31, 23, 15, 2},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
runParseTestCase(t, tc)
})
}
}
func TestParseFilenameTimestampsErrors(t *testing.T) {
t.Run("should throw error for empty filename array", func(t *testing.T) {
_, err := ParseFilenameTimestamps([]string{})
if err == nil {
t.Error("Expected error for empty filename array")
}
if err != nil && err.Error() != "no filenames provided" {
t.Logf("Error message: %v", err)
}
})
t.Run("should throw error for filenames without date patterns", func(t *testing.T) {
_, err := ParseFilenameTimestamps([]string{"invalid_filename.wav"})
if err == nil {
t.Error("Expected error for filenames without date patterns")
}
})
t.Run("should throw error for mixed date formats", func(t *testing.T) {
mixedFormats := []string{"201012_123456.wav", "20231012_123456.wav"} _, err := ParseFilenameTimestamps(mixedFormats)
if err == nil {
t.Error("Expected error for mixed date formats")
}
})
t.Run("should throw error for wrong length patterns", func(t *testing.T) {
wrongLength := []string{"2010_123456.wav"} _, err := ParseFilenameTimestamps(wrongLength)
if err == nil {
t.Error("Expected error for wrong length patterns")
}
})
t.Run("should throw error when not enough files for 6-digit disambiguation", func(t *testing.T) {
singleFile := []string{"120119_003002.wav"}
_, err := ParseFilenameTimestamps(singleFile)
if err == nil {
t.Error("Expected error when not enough files for 6-digit disambiguation")
}
})
}
func TestApplyTimezoneOffset(t *testing.T) {
t.Run("should apply UTC timezone correctly", func(t *testing.T) {
results := parseAndApply(t, []string{"201012_123456.wav", "201014_123456.WAV"}, "UTC")
if len(results) != 2 {
t.Fatalf("Expected 2 results, got %d", len(results))
}
assertOffset(t, results[0], 0)
})
t.Run("should use fixed offset for entire cluster spanning DST transition", func(t *testing.T) {
results := parseAndApply(t, []string{
"20210401_120000.wav", "20210410_120000.wav", "20210420_120000.wav", }, "Pacific/Auckland")
if len(results) != 3 {
t.Fatalf("Expected 3 results, got %d", len(results))
}
for _, r := range results {
assertOffset(t, r, 13*3600)
}
assertTimestamp(t, results[0].UTC(), expectedTS{2021, 3, 31, 23, 0, 0})
assertTimestamp(t, results[1].UTC(), expectedTS{2021, 4, 9, 23, 0, 0})
assertTimestamp(t, results[2].UTC(), expectedTS{2021, 4, 19, 23, 0, 0})
})
t.Run("should handle out-of-order filenames correctly", func(t *testing.T) {
results := parseAndApply(t, []string{
"20210410_120000.wav", "20210401_120000.wav", "20210405_120000.wav", }, "Pacific/Auckland")
for _, r := range results {
assertOffset(t, r, 13*3600)
}
assertTimestamp(t, results[0], expectedTS{2021, 4, 10, 12, 0, 0})
assertTimestamp(t, results[1], expectedTS{2021, 4, 1, 12, 0, 0})
assertTimestamp(t, results[2], expectedTS{2021, 4, 5, 12, 0, 0})
})
t.Run("should apply fixed offset consistently across large time spans", func(t *testing.T) {
results := parseAndApply(t, []string{
"20210215_120000.wav", "20210615_120000.wav", "20210815_120000.wav", }, "Pacific/Auckland")
for _, r := range results {
assertOffset(t, r, 13*3600)
}
assertTimestamp(t, results[0].UTC(), expectedTS{2021, 2, 14, 23, 0, 0})
assertTimestamp(t, results[1].UTC(), expectedTS{2021, 6, 14, 23, 0, 0})
assertTimestamp(t, results[2].UTC(), expectedTS{2021, 8, 14, 23, 0, 0})
})
t.Run("should handle US DST transitions with fixed offset", func(t *testing.T) {
results := parseAndApply(t, []string{
"20210310_120000.wav", "20210320_120000.wav", }, "America/New_York")
for _, r := range results {
assertOffset(t, r, -5*3600)
}
assertTimestamp(t, results[0].UTC(), expectedTS{2021, 3, 10, 17, 0, 0})
assertTimestamp(t, results[1].UTC(), expectedTS{2021, 3, 20, 17, 0, 0})
})
t.Run("should handle empty timestamps array", func(t *testing.T) {
_, err := ApplyTimezoneOffset([]FilenameTimestamp{}, "UTC")
if err == nil {
t.Error("Expected error for empty timestamps array")
}
})
t.Run("should handle invalid timezone", func(t *testing.T) {
filenames := []string{"20210401_120000.wav"}
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
_, err = ApplyTimezoneOffset(parsed, "Invalid/Timezone")
if err == nil {
t.Error("Expected error for invalid timezone")
}
})
}
func TestHasTimestampFilename(t *testing.T) {
testCases := []struct {
filename string
expected bool
}{
{"201012_123456.wav", true},
{"20230609_103000.WAV", true},
{"invalid_filename.wav", false},
{"201012_123456.txt", false},
{"201012.wav", false},
{"_123456.wav", false},
{"", false},
}
for _, tc := range testCases {
t.Run(tc.filename, func(t *testing.T) {
result := HasTimestampFilename(tc.filename)
if result != tc.expected {
t.Errorf("HasTimestampFilename(%q) = %v, want %v", tc.filename, result, tc.expected)
}
})
}
}
func TestFilenameParserEdgeCases(t *testing.T) {
t.Run("should handle case-insensitive file extensions", func(t *testing.T) {
filenames := []string{
"201012_123456.wav",
"201014_123456.WAV",
"201217_123456.Wav",
}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != 3 {
t.Errorf("Expected 3 results, got %d", len(results))
}
})
t.Run("should validate invalid dates", func(t *testing.T) {
filenames := []string{"20240132_120000.wav"}
_, err := ParseFilenameTimestamps(filenames)
if err == nil {
t.Error("Expected error for invalid date (day 32)")
}
})
t.Run("should validate invalid months", func(t *testing.T) {
filenames := []string{"20241301_120000.wav"}
_, err := ParseFilenameTimestamps(filenames)
if err == nil {
t.Error("Expected error for invalid month (13)")
}
})
t.Run("should handle February 29th in leap year", func(t *testing.T) {
filenames := []string{"20240229_120000.wav"}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse leap year date: %v", err)
}
if results[0].Timestamp.Day() != 29 {
t.Errorf("Expected day 29, got %d", results[0].Timestamp.Day())
}
})
t.Run("should reject February 29th in non-leap year", func(t *testing.T) {
filenames := []string{"20230229_120000.wav"}
_, err := ParseFilenameTimestamps(filenames)
if err == nil {
t.Error("Expected error for Feb 29th in non-leap year")
}
})
}
func TestUTCConversionCorrectness(t *testing.T) {
t.Run("should convert Pacific/Auckland night recordings correctly to UTC", func(t *testing.T) {
results := parseAndApply(t, []string{"20210505_210000.wav"}, "Pacific/Auckland")
assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 9, 0, 0})
})
t.Run("should convert day recordings correctly to UTC", func(t *testing.T) {
results := parseAndApply(t, []string{"20210505_120000.wav"}, "Pacific/Auckland")
assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 0, 0, 0})
})
t.Run("should handle date rollover correctly", func(t *testing.T) {
results := parseAndApply(t, []string{"20210505_020000.wav"}, "Pacific/Auckland")
assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 4, 14, 0, 0})
})
t.Run("should convert correctly for negative offset timezone", func(t *testing.T) {
results := parseAndApply(t, []string{"20210615_150000.wav"}, "America/New_York")
assertTimestamp(t, results[0].UTC(), expectedTS{2021, 6, 15, 19, 0, 0})
})
}