package utils
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
}
func TestParseFilenameTimestamps(t *testing.T) {
t.Run("should parse YYMMDD format (test case a)", func(t *testing.T) {
filenames := []string{
"201012_123456.wav",
"201014_123456.WAV",
"201217_123456.wav",
"211122_123456.WAV",
}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != 4 {
t.Fatalf("Expected 4 results, got %d", len(results))
}
assertTimestamp(t, results[0].Timestamp, expectedTS{2020, 10, 12, 12, 34, 56})
assertTimestamp(t, results[3].Timestamp, expectedTS{2021, 11, 22, 12, 34, 56})
})
t.Run("should parse DDMMYY format (test case b)", func(t *testing.T) {
filenames := []string{
"121020_123456.WAV",
"141020_123456.wav",
"171220_123456.WAV",
"221121_123456.wav",
}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != 4 {
t.Fatalf("Expected 4 results, got %d", len(results))
}
assertTimestamp(t, results[0].Timestamp, expectedTS{2020, 10, 12, 12, 34, 56})
assertTimestamp(t, results[2].Timestamp, expectedTS{2020, 12, 17, 12, 34, 56})
})
t.Run("should parse YYYYMMDD format (test case c)", func(t *testing.T) {
filenames := []string{
"20230609_103000.WAV",
"20241109_201504.wav",
}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != 2 {
t.Fatalf("Expected 2 results, got %d", len(results))
}
assertTimestamp(t, results[0].Timestamp, expectedTS{2023, 6, 9, 10, 30, 0})
assertTimestamp(t, results[1].Timestamp, expectedTS{2024, 11, 9, 20, 15, 4})
})
t.Run("should parse mixed 6-digit dates with variance detection (test case d)", func(t *testing.T) {
filenames := []string{
"120119_003002.wav",
"180120_231502.wav",
"170122_010005.wav",
"010419_234502.WAV",
"310320_231502.wav",
"220824_231502.WAV",
"240123_231502.wav",
}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != 7 {
t.Fatalf("Expected 7 results, got %d", len(results))
}
assertTimestamp(t, results[0].Timestamp, expectedTS{2019, 1, 12, 0, 30, 2})
assertTimestamp(t, results[4].Timestamp, expectedTS{2020, 3, 31, 23, 15, 2})
})
t.Run("should parse filenames with prefixes (test case e)", func(t *testing.T) {
filenames := []string{
"XYZ123_7689_20230609_103000.WAV",
"string 20241109_201504.wav",
}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != 2 {
t.Fatalf("Expected 2 results, got %d", len(results))
}
assertTimestamp(t, results[0].Timestamp, expectedTS{2023, 6, 9, 10, 30, 0})
assertTimestamp(t, results[1].Timestamp, expectedTS{2024, 11, 9, 20, 15, 4})
})
t.Run("should parse filenames with complex prefixes (test case f)", func(t *testing.T) {
filenames := []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",
}
results, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
if len(results) != 7 {
t.Fatalf("Expected 7 results, got %d", len(results))
}
assertTimestamp(t, results[0].Timestamp, expectedTS{2020, 1, 18, 23, 15, 2})
assertTimestamp(t, results[1].Timestamp, expectedTS{2019, 1, 12, 0, 30, 2})
assertTimestamp(t, results[4].Timestamp, expectedTS{2020, 3, 31, 23, 15, 2})
})
}
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})
})
}