package utils
import (
"testing"
)
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))
}
if results[0].Timestamp.Year() != 2020 {
t.Errorf("Year incorrect for file 0: got %d, want 2020", results[0].Timestamp.Year())
}
if results[0].Timestamp.Month() != 10 { t.Errorf("Month incorrect for file 0: got %d, want 10", results[0].Timestamp.Month())
}
if results[0].Timestamp.Day() != 12 {
t.Errorf("Day incorrect for file 0: got %d, want 12", results[0].Timestamp.Day())
}
if results[0].Timestamp.Hour() != 12 {
t.Errorf("Hour incorrect for file 0: got %d, want 12", results[0].Timestamp.Hour())
}
if results[0].Timestamp.Minute() != 34 {
t.Errorf("Minute incorrect for file 0: got %d, want 34", results[0].Timestamp.Minute())
}
if results[0].Timestamp.Second() != 56 {
t.Errorf("Second incorrect for file 0: got %d, want 56", results[0].Timestamp.Second())
}
if results[3].Timestamp.Year() != 2021 {
t.Errorf("Year incorrect for file 3: got %d, want 2021", results[3].Timestamp.Year())
}
if results[3].Timestamp.Month() != 11 { t.Errorf("Month incorrect for file 3: got %d, want 11", results[3].Timestamp.Month())
}
if results[3].Timestamp.Day() != 22 {
t.Errorf("Day incorrect for file 3: got %d, want 22", results[3].Timestamp.Day())
}
})
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))
}
if results[0].Timestamp.Day() != 12 {
t.Errorf("Day incorrect for file 0: got %d, want 12", results[0].Timestamp.Day())
}
if results[0].Timestamp.Month() != 10 { t.Errorf("Month incorrect for file 0: got %d, want 10", results[0].Timestamp.Month())
}
if results[0].Timestamp.Year() != 2020 {
t.Errorf("Year incorrect for file 0: got %d, want 2020", results[0].Timestamp.Year())
}
if results[2].Timestamp.Day() != 17 {
t.Errorf("Day incorrect for file 2: got %d, want 17", results[2].Timestamp.Day())
}
if results[2].Timestamp.Month() != 12 { t.Errorf("Month incorrect for file 2: got %d, want 12", results[2].Timestamp.Month())
}
if results[2].Timestamp.Year() != 2020 {
t.Errorf("Year incorrect for file 2: got %d, want 2020", results[2].Timestamp.Year())
}
})
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))
}
if results[0].Timestamp.Year() != 2023 {
t.Errorf("Year incorrect: got %d, want 2023", results[0].Timestamp.Year())
}
if results[0].Timestamp.Month() != 6 { t.Errorf("Month incorrect: got %d, want 6", results[0].Timestamp.Month())
}
if results[0].Timestamp.Day() != 9 {
t.Errorf("Day incorrect: got %d, want 9", results[0].Timestamp.Day())
}
if results[0].Timestamp.Hour() != 10 {
t.Errorf("Hour incorrect: got %d, want 10", results[0].Timestamp.Hour())
}
if results[0].Timestamp.Minute() != 30 {
t.Errorf("Minute incorrect: got %d, want 30", results[0].Timestamp.Minute())
}
if results[0].Timestamp.Second() != 0 {
t.Errorf("Second incorrect: got %d, want 0", results[0].Timestamp.Second())
}
if results[1].Timestamp.Year() != 2024 {
t.Errorf("Year incorrect: got %d, want 2024", results[1].Timestamp.Year())
}
})
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))
}
if results[0].Timestamp.Day() != 12 {
t.Errorf("Day incorrect: got %d, want 12", results[0].Timestamp.Day())
}
if results[0].Timestamp.Month() != 1 { t.Errorf("Month incorrect: got %d, want 1", results[0].Timestamp.Month())
}
if results[0].Timestamp.Year() != 2019 {
t.Errorf("Year incorrect: got %d, want 2019", results[0].Timestamp.Year())
}
if results[4].Timestamp.Day() != 31 {
t.Errorf("Day incorrect for file 4: got %d, want 31", results[4].Timestamp.Day())
}
if results[4].Timestamp.Month() != 3 { t.Errorf("Month incorrect for file 4: got %d, want 3", results[4].Timestamp.Month())
}
})
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 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))
}
if results[0].Timestamp.Year() != 2023 {
t.Errorf("Year incorrect: got %d, want 2023", results[0].Timestamp.Year())
}
if results[0].Timestamp.Month() != 6 { t.Errorf("Month incorrect: got %d, want 6", results[0].Timestamp.Month())
}
if results[0].Timestamp.Day() != 9 {
t.Errorf("Day incorrect: got %d, want 9", results[0].Timestamp.Day())
}
if results[0].Timestamp.Hour() != 10 {
t.Errorf("Hour incorrect: got %d, want 10", results[0].Timestamp.Hour())
}
if results[0].Timestamp.Minute() != 30 {
t.Errorf("Minute incorrect: got %d, want 30", results[0].Timestamp.Minute())
}
if results[0].Timestamp.Second() != 0 {
t.Errorf("Second incorrect: got %d, want 0", results[0].Timestamp.Second())
}
if results[1].Timestamp.Year() != 2024 {
t.Errorf("Year incorrect: got %d, want 2024", results[1].Timestamp.Year())
}
if results[1].Timestamp.Month() != 11 { t.Errorf("Month incorrect: got %d, want 11", results[1].Timestamp.Month())
}
if results[1].Timestamp.Day() != 9 {
t.Errorf("Day incorrect: got %d, want 9", results[1].Timestamp.Day())
}
if results[1].Timestamp.Hour() != 20 {
t.Errorf("Hour incorrect: got %d, want 20", results[1].Timestamp.Hour())
}
if results[1].Timestamp.Minute() != 15 {
t.Errorf("Minute incorrect: got %d, want 15", results[1].Timestamp.Minute())
}
if results[1].Timestamp.Second() != 4 {
t.Errorf("Second incorrect: got %d, want 4", results[1].Timestamp.Second())
}
})
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))
}
if results[0].Timestamp.Day() != 18 {
t.Errorf("Day incorrect: got %d, want 18", results[0].Timestamp.Day())
}
if results[0].Timestamp.Month() != 1 { t.Errorf("Month incorrect: got %d, want 1", results[0].Timestamp.Month())
}
if results[0].Timestamp.Year() != 2020 {
t.Errorf("Year incorrect: got %d, want 2020", results[0].Timestamp.Year())
}
if results[0].Timestamp.Hour() != 23 {
t.Errorf("Hour incorrect: got %d, want 23", results[0].Timestamp.Hour())
}
if results[0].Timestamp.Minute() != 15 {
t.Errorf("Minute incorrect: got %d, want 15", results[0].Timestamp.Minute())
}
if results[0].Timestamp.Second() != 2 {
t.Errorf("Second incorrect: got %d, want 2", results[0].Timestamp.Second())
}
if results[1].Timestamp.Day() != 12 {
t.Errorf("Day incorrect: got %d, want 12", results[1].Timestamp.Day())
}
if results[1].Timestamp.Month() != 1 { t.Errorf("Month incorrect: got %d, want 1", results[1].Timestamp.Month())
}
if results[1].Timestamp.Year() != 2019 {
t.Errorf("Year incorrect: got %d, want 2019", results[1].Timestamp.Year())
}
if results[4].Timestamp.Day() != 31 {
t.Errorf("Day incorrect: got %d, want 31", results[4].Timestamp.Day())
}
if results[4].Timestamp.Month() != 3 { t.Errorf("Month incorrect: got %d, want 3", results[4].Timestamp.Month())
}
if results[4].Timestamp.Year() != 2020 {
t.Errorf("Year incorrect: got %d, want 2020", results[4].Timestamp.Year())
}
})
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) {
filenames := []string{
"201012_123456.wav",
"201014_123456.WAV",
}
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "UTC")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
if len(results) != 2 {
t.Fatalf("Expected 2 results, got %d", len(results))
}
_, offset := results[0].Zone()
if offset != 0 {
t.Errorf("UTC offset should be 0, got %d", offset)
}
})
t.Run("should use fixed offset for entire cluster spanning DST transition", func(t *testing.T) {
filenames := []string{
"20210401_120000.wav", "20210410_120000.wav", "20210420_120000.wav", }
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "Pacific/Auckland")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
if len(results) != 3 {
t.Fatalf("Expected 3 results, got %d", len(results))
}
offsets := make([]int, len(results))
for i, r := range results {
_, offset := r.Zone()
offsets[i] = offset
}
firstOffset := offsets[0]
for i, offset := range offsets {
if offset != firstOffset {
t.Errorf("File %d has different offset: got %d, want %d", i, offset, firstOffset)
}
}
expectedOffsetSeconds := 13 * 3600
if firstOffset != expectedOffsetSeconds {
t.Errorf("Offset incorrect: got %d seconds, want %d seconds (UTC+13)", firstOffset, expectedOffsetSeconds)
}
for i, utcTime := range results {
utc := utcTime.UTC()
if utc.Hour() != 23 {
t.Errorf("File %d UTC hour incorrect: got %d, want 23", i, utc.Hour())
}
}
})
t.Run("should handle out-of-order filenames correctly", func(t *testing.T) {
filenames := []string{
"20210410_120000.wav", "20210401_120000.wav", "20210405_120000.wav", }
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "Pacific/Auckland")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
for i, r := range results {
_, offset := r.Zone()
expectedOffset := 13 * 3600
if offset != expectedOffset {
t.Errorf("File %d offset incorrect: got %d, want %d", i, offset, expectedOffset)
}
}
if results[0].Day() != 10 {
t.Errorf("Result 0 should be April 10th, got day %d", results[0].Day())
}
if results[1].Day() != 1 {
t.Errorf("Result 1 should be April 1st, got day %d", results[1].Day())
}
if results[2].Day() != 5 {
t.Errorf("Result 2 should be April 5th, got day %d", results[2].Day())
}
})
t.Run("should apply fixed offset consistently across large time spans", func(t *testing.T) {
filenames := []string{
"20210215_120000.wav", "20210615_120000.wav", "20210815_120000.wav", }
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "Pacific/Auckland")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
expectedOffset := 13 * 3600
for i, r := range results {
_, offset := r.Zone()
if offset != expectedOffset {
t.Errorf("File %d offset incorrect: got %d, want %d", i, offset, expectedOffset)
}
}
for i, r := range results {
utc := r.UTC()
if utc.Hour() != 23 { t.Errorf("File %d UTC hour incorrect: got %d, want 23", i, utc.Hour())
}
}
})
t.Run("should handle US DST transitions with fixed offset", func(t *testing.T) {
filenames := []string{
"20210310_120000.wav", "20210320_120000.wav", }
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "America/New_York")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
expectedOffset := -5 * 3600
for i, r := range results {
_, offset := r.Zone()
if offset != expectedOffset {
t.Errorf("File %d offset incorrect: got %d, want %d", i, offset, expectedOffset)
}
}
for i, r := range results {
utc := r.UTC()
if utc.Hour() != 17 { t.Errorf("File %d UTC hour incorrect: got %d, want 17", i, utc.Hour())
}
}
})
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) {
filenames := []string{"20210505_210000.wav"}
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "Pacific/Auckland")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
utcDate := results[0].UTC()
if utcDate.Year() != 2021 {
t.Errorf("Year incorrect: got %d, want 2021", utcDate.Year())
}
if utcDate.Month() != 5 {
t.Errorf("Month incorrect: got %d, want 5", utcDate.Month())
}
if utcDate.Day() != 5 {
t.Errorf("Day incorrect: got %d, want 5 (same day)", utcDate.Day())
}
if utcDate.Hour() != 9 {
t.Errorf("Hour incorrect: got %d, want 9 (21 - 12 = 9)", utcDate.Hour())
}
})
t.Run("should convert day recordings correctly to UTC", func(t *testing.T) {
filenames := []string{"20210505_120000.wav"}
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "Pacific/Auckland")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
utcDate := results[0].UTC()
if utcDate.Hour() != 0 {
t.Errorf("Hour incorrect: got %d, want 0 (12 - 12 = 0, midnight UTC)", utcDate.Hour())
}
if utcDate.Day() != 5 {
t.Errorf("Day incorrect: got %d, want 5 (same day)", utcDate.Day())
}
})
t.Run("should handle date rollover correctly", func(t *testing.T) {
filenames := []string{"20210505_020000.wav"}
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "Pacific/Auckland")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
utcDate := results[0].UTC()
if utcDate.Day() != 4 {
t.Errorf("Day incorrect: got %d, want 4 (previous day)", utcDate.Day())
}
if utcDate.Hour() != 14 {
t.Errorf("Hour incorrect: got %d, want 14 (2 - 12 = -10, so previous day 14:00)", utcDate.Hour())
}
})
t.Run("should convert correctly for negative offset timezone", func(t *testing.T) {
filenames := []string{"20210615_150000.wav"}
parsed, err := ParseFilenameTimestamps(filenames)
if err != nil {
t.Fatalf("Failed to parse filenames: %v", err)
}
results, err := ApplyTimezoneOffset(parsed, "America/New_York")
if err != nil {
t.Fatalf("Failed to apply timezone: %v", err)
}
utcDate := results[0].UTC()
if utcDate.Hour() != 19 {
t.Errorf("Hour incorrect: got %d, want 19 (15 + 4 = 19)", utcDate.Hour())
}
if utcDate.Day() != 15 {
t.Errorf("Day incorrect: got %d, want 15 (same day)", utcDate.Day())
}
})
}