cyclomatic complexity
Dependencies
- [2]
LQLC7S3Atrying gemini: Inconsistent Standards in @utils/ refactoring - [3]
EA6OQXWFadded makefile to count loc and run shell scripts - [4]
54GPBNIXadded +_ for tui to select segments with no calltype - [5]
KZKLAINJrun out of space on nest, cleaned out - [*]
SJN7IKIV
Change contents
- replacement in utils/wav_metadata.go at line 417
startOffset = startSample * int64(blockAlign)if startOffset > dataSize {startOffset = dataSize}startOffset = min(startSample*int64(blockAlign), dataSize) - replacement in utils/wav_metadata.go at line 422
endOffset := endSample * int64(blockAlign)if endOffset > dataSize {endOffset = dataSize}endOffset := min(endSample*int64(blockAlign), dataSize) - edit in utils/filename_parser_test.go at line 5
"time" - edit in utils/filename_parser_test.go at line 8
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)}}// parseAndApply is a test helper that parses filenames and applies a timezone offset.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} - replacement in utils/filename_parser_test.go at line 76
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 { // Octobert.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 { // Novembert.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())}assertTimestamp(t, results[0].Timestamp, expectedTS{2020, 10, 12, 12, 34, 56})assertTimestamp(t, results[3].Timestamp, expectedTS{2021, 11, 22, 12, 34, 56}) - replacement in utils/filename_parser_test.go at line 99
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 { // Octobert.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 { // Decembert.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())}assertTimestamp(t, results[0].Timestamp, expectedTS{2020, 10, 12, 12, 34, 56})assertTimestamp(t, results[2].Timestamp, expectedTS{2020, 12, 17, 12, 34, 56}) - replacement in utils/filename_parser_test.go at line 118
if results[0].Timestamp.Year() != 2023 {t.Errorf("Year incorrect: got %d, want 2023", results[0].Timestamp.Year())}if results[0].Timestamp.Month() != 6 { // Junet.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())}assertTimestamp(t, results[0].Timestamp, expectedTS{2023, 6, 9, 10, 30, 0})assertTimestamp(t, results[1].Timestamp, expectedTS{2024, 11, 9, 20, 15, 4}) - replacement in utils/filename_parser_test.go at line 145
if results[0].Timestamp.Day() != 12 {t.Errorf("Day incorrect: got %d, want 12", results[0].Timestamp.Day())}if results[0].Timestamp.Month() != 1 { // Januaryt.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 { // Marcht.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)}assertTimestamp(t, results[0].Timestamp, expectedTS{2019, 1, 12, 0, 30, 2})assertTimestamp(t, results[4].Timestamp, expectedTS{2020, 3, 31, 23, 15, 2}) - edit in utils/filename_parser_test.go at line 149
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")}}) - replacement in utils/filename_parser_test.go at line 164
if results[0].Timestamp.Year() != 2023 {t.Errorf("Year incorrect: got %d, want 2023", results[0].Timestamp.Year())}if results[0].Timestamp.Month() != 6 { // Junet.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 { // Novembert.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())}assertTimestamp(t, results[0].Timestamp, expectedTS{2023, 6, 9, 10, 30, 0})assertTimestamp(t, results[1].Timestamp, expectedTS{2024, 11, 9, 20, 15, 4}) - replacement in utils/filename_parser_test.go at line 189
if results[0].Timestamp.Day() != 18 {t.Errorf("Day incorrect: got %d, want 18", results[0].Timestamp.Day())}if results[0].Timestamp.Month() != 1 { // Januaryt.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())}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})})} - replacement in utils/filename_parser_test.go at line 195
if results[1].Timestamp.Day() != 12 {t.Errorf("Day incorrect: got %d, want 12", results[1].Timestamp.Day())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") - replacement in utils/filename_parser_test.go at line 201
if results[1].Timestamp.Month() != 1 { // Januaryt.Errorf("Month incorrect: got %d, want 1", results[1].Timestamp.Month())if err != nil && err.Error() != "no filenames provided" {t.Logf("Error message: %v", err) - replacement in utils/filename_parser_test.go at line 204
if results[1].Timestamp.Year() != 2019 {t.Errorf("Year incorrect: got %d, want 2019", results[1].Timestamp.Year())}}) - replacement in utils/filename_parser_test.go at line 206
if results[4].Timestamp.Day() != 31 {t.Errorf("Day incorrect: got %d, want 31", results[4].Timestamp.Day())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") - edit in utils/filename_parser_test.go at line 211
if results[4].Timestamp.Month() != 3 { // Marcht.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())} - replacement in utils/filename_parser_test.go at line 240
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)}results := parseAndApply(t, []string{"201012_123456.wav", "201014_123456.WAV"}, "UTC") - replacement in utils/filename_parser_test.go at line 244
// Check timezone offset is +00:00_, offset := results[0].Zone()if offset != 0 {t.Errorf("UTC offset should be 0, got %d", offset)}assertOffset(t, results[0], 0) - replacement in utils/filename_parser_test.go at line 248
// Test files spanning the Auckland DST transition in April 2021// DST ended on April 4, 2021 (UTC+13 -> UTC+12)filenames := []string{"20210401_120000.wav", // April 1st - DST still active (UTC+13)// Auckland DST ended April 4, 2021 (UTC+13 -> UTC+12)results := parseAndApply(t, []string{"20210401_120000.wav", // April 1st - DST active (UTC+13) - replacement in utils/filename_parser_test.go at line 252
"20210420_120000.wav", // April 20th - Standard time (would be UTC+12 if DST applied)}parsed, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}"20210420_120000.wav", // April 20th - Standard time}, "Pacific/Auckland") - edit in utils/filename_parser_test.go at line 255
results, err := ApplyTimezoneOffset(parsed, "Pacific/Auckland")if err != nil {t.Fatalf("Failed to apply timezone: %v", err)} - replacement in utils/filename_parser_test.go at line 259
// All files should use the same offset (from April 1st - earliest file)offsets := make([]int, len(results))for i, r := range results {_, offset := r.Zone()offsets[i] = offset// All files should use UTC+13 offset (from earliest file: April 1st)for _, r := range results {assertOffset(t, r, 13*3600) - replacement in utils/filename_parser_test.go at line 264
// Check all offsets are the samefirstOffset := offsets[0]for i, offset := range offsets {if offset != firstOffset {t.Errorf("File %d has different offset: got %d, want %d", i, offset, firstOffset)}}// The offset should be UTC+13 (from the earliest file: April 1st)expectedOffsetSeconds := 13 * 3600if firstOffset != expectedOffsetSeconds {t.Errorf("Offset incorrect: got %d seconds, want %d seconds (UTC+13)", firstOffset, expectedOffsetSeconds)}// Verify UTC conversion uses the fixed offset consistently// All files at 12:00 local should convert to the same UTC hour (with UTC+13 offset)// 12:00 Auckland time - 13 hours = 23:00 UTC previous dayfor 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())}}// All at 12:00 local - 13h = 23:00 UTC previous dayassertTimestamp(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}) - replacement in utils/filename_parser_test.go at line 271
// Files not in chronological order - should still use earliest file for offsetfilenames := []string{results := parseAndApply(t, []string{ - replacement in utils/filename_parser_test.go at line 273
"20210401_120000.wav", // April 1st (earliest - should determine offset)"20210401_120000.wav", // April 1st (earliest - determines offset) - replacement in utils/filename_parser_test.go at line 275
}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)}}, "Pacific/Auckland") - replacement in utils/filename_parser_test.go at line 277
// All files should use UTC+13 offset (from April 1st, the earliest)for i, r := range results {_, offset := r.Zone()expectedOffset := 13 * 3600if offset != expectedOffset {t.Errorf("File %d offset incorrect: got %d, want %d", i, offset, expectedOffset)}// All files use UTC+13 (from April 1st, the earliest)for _, r := range results {assertOffset(t, r, 13*3600) - replacement in utils/filename_parser_test.go at line 282
// Results should maintain original filename orderif 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())}// Results maintain original filename orderassertTimestamp(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}) - replacement in utils/filename_parser_test.go at line 289
// Test files spanning multiple months with different DST periodsfilenames := []string{"20210215_120000.wav", // February 15th (summer, UTC+13)"20210615_120000.wav", // June 15th (winter, would be UTC+12 if DST applied)"20210815_120000.wav", // August 15th (winter, would be UTC+12 if DST applied)}results := parseAndApply(t, []string{"20210215_120000.wav", // February (summer, UTC+13)"20210615_120000.wav", // June (winter, would be UTC+12 if DST applied)"20210815_120000.wav", // August (winter)}, "Pacific/Auckland") - replacement in utils/filename_parser_test.go at line 295
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)}// All files should use the same offset from the earliest file (February)expectedOffset := 13 * 3600for i, r := range results {_, offset := r.Zone()if offset != expectedOffset {t.Errorf("File %d offset incorrect: got %d, want %d", i, offset, expectedOffset)}// All files use offset from earliest (February): UTC+13for _, r := range results {assertOffset(t, r, 13*3600) - replacement in utils/filename_parser_test.go at line 300
// Verify UTC conversion is consistent with fixed offsetfor i, r := range results {utc := r.UTC()if utc.Hour() != 23 { // 12 - 13 = -1 hour (23:00 previous day)t.Errorf("File %d UTC hour incorrect: got %d, want 23", i, utc.Hour())}}// 12:00 local - 13h = 23:00 UTC previous dayassertTimestamp(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}) - replacement in utils/filename_parser_test.go at line 307
// Test US spring DST transition (March 14, 2021)filenames := []string{results := parseAndApply(t, []string{ - replacement in utils/filename_parser_test.go at line 309
"20210320_120000.wav", // March 20th - after DST (would be UTC-4 if DST applied)}parsed, err := ParseFilenameTimestamps(filenames)if err != nil {t.Fatalf("Failed to parse filenames: %v", err)}"20210320_120000.wav", // March 20th - after DST (would be UTC-4)}, "America/New_York") - replacement in utils/filename_parser_test.go at line 312
results, err := ApplyTimezoneOffset(parsed, "America/New_York")if err != nil {t.Fatalf("Failed to apply timezone: %v", err)// All files use offset from earliest (March 10th): UTC-5for _, r := range results {assertOffset(t, r, -5*3600) - replacement in utils/filename_parser_test.go at line 317
// All files should use the same offset from earliest file (March 10th)expectedOffset := -5 * 3600for i, r := range results {_, offset := r.Zone()if offset != expectedOffset {t.Errorf("File %d offset incorrect: got %d, want %d", i, offset, expectedOffset)}}// Verify UTC conversion uses fixed offsetfor i, r := range results {utc := r.UTC()if utc.Hour() != 17 { // 12 + 5 = 17t.Errorf("File %d UTC hour incorrect: got %d, want 17", i, utc.Hour())}}// 12:00 local + 5h = 17:00 UTCassertTimestamp(t, results[0].UTC(), expectedTS{2021, 3, 10, 17, 0, 0})assertTimestamp(t, results[1].UTC(), expectedTS{2021, 3, 20, 17, 0, 0}) - replacement in utils/filename_parser_test.go at line 430
// Test a night recording: 21:00 (9 PM) Pacific/Auckland// In May 2021, Pacific/Auckland is UTC+12 (standard time)// So 21:00 Pacific/Auckland should become 09:00 UTC same dayfilenames := []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())}// 21:00 Pacific/Auckland (May = UTC+12) → 09:00 UTC same dayresults := parseAndApply(t, []string{"20210505_210000.wav"}, "Pacific/Auckland")assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 9, 0, 0}) - replacement in utils/filename_parser_test.go at line 436
// Test a day recording: 12:00 (noon) Pacific/Auckland// Should become 00:00 UTC same day (midnight)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())}// 12:00 Pacific/Auckland (May = UTC+12) → 00:00 UTC same dayresults := parseAndApply(t, []string{"20210505_120000.wav"}, "Pacific/Auckland")assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 5, 0, 0, 0}) - replacement in utils/filename_parser_test.go at line 442
// Test early morning: 02:00 Pacific/Auckland// Should become 14:00 UTC previous dayfilenames := []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())}// 02:00 Pacific/Auckland (May = UTC+12) → 14:00 UTC previous dayresults := parseAndApply(t, []string{"20210505_020000.wav"}, "Pacific/Auckland")assertTimestamp(t, results[0].UTC(), expectedTS{2021, 5, 4, 14, 0, 0}) - replacement in utils/filename_parser_test.go at line 448
// Test 15:00 (3 PM) New York in June (UTC-4 during DST)// Should become 19:00 UTC same dayfilenames := []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())}// 15:00 New York (June = UTC-4 during DST) → 19:00 UTC same dayresults := parseAndApply(t, []string{"20210615_150000.wav"}, "America/New_York")assertTimestamp(t, results[0].UTC(), expectedTS{2021, 6, 15, 19, 0, 0}) - edit in lint_test.go at line 37[4.776924]
func TestFix(t *testing.T) {cmd := exec.Command("go", "fix", "./...")cmd.Dir = "."out, err := cmd.CombinedOutput()if err != nil {t.Errorf("go fix failed:\n%s", out)}} - replacement in cmd/calls_classify.go at line 66
// RunCallsClassify handles the "calls classify" subcommandfunc RunCallsClassify(args []string) {var folder, file, filter, species, gotoFile, timezone stringvar certainty, sample intvar night, day boolvar lat, lng float64var latSet, lngSet bool// classifyArgs holds parsed CLI arguments for the classify subcommand.type classifyArgs struct {folder stringfile stringfilter stringspecies stringgotoFile stringtimezone stringcertainty intsample intnight boolday boollat float64lng float64latSet boollngSet bool} - replacement in cmd/calls_classify.go at line 84
// Default to -1 (no filter / no sampling)certainty = -1sample = -1// parseClassifyArgs parses the argument slice and returns classified args.// Exits on parse errors.func parseClassifyArgs(args []string) classifyArgs {a := classifyArgs{certainty: -1, sample: -1} - edit in cmd/calls_classify.go at line 89
// Parse arguments - replacement in cmd/calls_classify.go at line 95
if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --folder requires a value\n")os.Exit(1)}folder = args[i+1]a.folder = a.requireValue(args, i, "--folder") - edit in cmd/calls_classify.go at line 97
- replacement in cmd/calls_classify.go at line 98
if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --file requires a value\n")os.Exit(1)}file = args[i+1]a.file = a.requireValue(args, i, "--file") - edit in cmd/calls_classify.go at line 100
- replacement in cmd/calls_classify.go at line 101
if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --filter requires a value\n")os.Exit(1)}if filter != "" {fmt.Fprintf(os.Stderr, "Error: --filter can only be specified once\n")os.Exit(1)}filter = args[i+1]a.filter = a.requireUniqueValue(args, i, "--filter", a.filter) - edit in cmd/calls_classify.go at line 103
- replacement in cmd/calls_classify.go at line 104
if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --species requires a value\n")os.Exit(1)}if species != "" {fmt.Fprintf(os.Stderr, "Error: --species can only be specified once\n")os.Exit(1)}species = args[i+1]a.species = a.requireUniqueValue(args, i, "--species", a.species) - edit in cmd/calls_classify.go at line 106
- replacement in cmd/calls_classify.go at line 107
if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --certainty requires a value\n")os.Exit(1)}v, err := strconv.Atoi(args[i+1])if err != nil {fmt.Fprintf(os.Stderr, "Error: --certainty must be an integer\n")os.Exit(1)}if v < 0 || v > 100 {fmt.Fprintf(os.Stderr, "Error: --certainty must be between 0 and 100\n")os.Exit(1)}certainty = va.certainty = a.requireIntRange(args, i, "--certainty", 0, 100) - replacement in cmd/calls_classify.go at line 109
case "--night":night = truei++case "--day":day = truei++case "--lat":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --lat requires a value\n")os.Exit(1)}v, err := strconv.ParseFloat(args[i+1], 64)if err != nil {fmt.Fprintf(os.Stderr, "Error: --lat must be a number\n")os.Exit(1)}lat = vlatSet = truecase "--sample":a.sample = a.requireIntRange(args, i, "--sample", 1, 100) - replacement in cmd/calls_classify.go at line 112
case "--lng":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --lng requires a value\n")os.Exit(1)}v, err := strconv.ParseFloat(args[i+1], 64)if err != nil {fmt.Fprintf(os.Stderr, "Error: --lng must be a number\n")os.Exit(1)}lng = vlngSet = truecase "--goto":a.gotoFile = a.requireValue(args, i, "--goto") - edit in cmd/calls_classify.go at line 115
- replacement in cmd/calls_classify.go at line 116
if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --timezone requires a value\n")os.Exit(1)}timezone = args[i+1]a.timezone = a.requireValue(args, i, "--timezone")i += 2case "--lat":a.lat = a.requireFloat(args, i, "--lat")a.latSet = truei += 2case "--lng":a.lng = a.requireFloat(args, i, "--lng")a.lngSet = true - replacement in cmd/calls_classify.go at line 126
case "--night":a.night = truei++case "--day":a.day = truei++ - edit in cmd/calls_classify.go at line 135
case "--sample":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --sample requires a value\n")os.Exit(1)}v, err := strconv.Atoi(args[i+1])if err != nil {fmt.Fprintf(os.Stderr, "Error: --sample must be an integer\n")os.Exit(1)}if v <= 0 || v > 100 {fmt.Fprintf(os.Stderr, "Error: --sample must be between 1 and 100\n")os.Exit(1)}sample = vi += 2case "--goto":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --goto requires a value\n")os.Exit(1)}gotoFile = args[i+1]i += 2 - replacement in cmd/calls_classify.go at line 142
// --sample 1-99 requires --certainty; --sample 100 is a no-opif sample > 0 && sample < 100 && certainty < 0 {fmt.Fprintf(os.Stderr, "Error: --sample requires --certainty to be set\n")return a}// requireValue returns the next argument or exits if missing.func (classifyArgs) requireValue(args []string, i int, flag string) string {if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flag) - edit in cmd/calls_classify.go at line 151
return args[i+1]} - replacement in cmd/calls_classify.go at line 154
// Validate required flagsif folder == "" && file == "" {fmt.Fprintf(os.Stderr, "Error: missing required flag: --folder or --file\n\n")printClassifyUsage()// requireUniqueValue is like requireValue but exits if the flag was already set.func (a classifyArgs) requireUniqueValue(args []string, i int, flag, current string) string {if current != "" {fmt.Fprintf(os.Stderr, "Error: %s can only be specified once\n", flag) - edit in cmd/calls_classify.go at line 160
return a.requireValue(args, i, flag)} - replacement in cmd/calls_classify.go at line 163
if night && day {fmt.Fprintf(os.Stderr, "Error: --night and --day are mutually exclusive\n\n")printClassifyUsage()// requireIntRange parses the next arg as int and validates range [lo,hi].func (a classifyArgs) requireIntRange(args []string, i int, flag string, lo, hi int) int {val := a.requireValue(args, i, flag)v, err := strconv.Atoi(val)if err != nil {fmt.Fprintf(os.Stderr, "Error: %s must be an integer\n", flag) - replacement in cmd/calls_classify.go at line 171
if (night || day) && (!latSet || !lngSet) {fmt.Fprintf(os.Stderr, "Error: --night/--day requires both --lat and --lng\n\n")printClassifyUsage()if v < lo || v > hi {fmt.Fprintf(os.Stderr, "Error: %s must be between %d and %d\n", flag, lo, hi) - edit in cmd/calls_classify.go at line 175
return v} - replacement in cmd/calls_classify.go at line 178
// Load reviewer, bindings, and display flags from ~/.skraak/config.json.cfg, cfgPath, err := utils.LoadConfig()// requireFloat parses the next arg as float64.func (a classifyArgs) requireFloat(args []string, i int, flag string) float64 {val := a.requireValue(args, i, flag)v, err := strconv.ParseFloat(val, 64) - replacement in cmd/calls_classify.go at line 183
fmt.Fprintf(os.Stderr, "Error: %v\n", err)fmt.Fprintf(os.Stderr, "Create %s with a \"classify\" section; run `skraak calls classify --help` for an example.\n", cfgPath)fmt.Fprintf(os.Stderr, "Error: %s must be a number\n", flag) - edit in cmd/calls_classify.go at line 186
return v} - replacement in cmd/calls_classify.go at line 189
// Validate config contentsif cfg.Classify.Reviewer == "" {fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.reviewer\"\n", cfgPath)// validate checks cross-flag constraints after parsing.func (a classifyArgs) validate() {if a.sample > 0 && a.sample < 100 && a.certainty < 0 {fmt.Fprintf(os.Stderr, "Error: --sample requires --certainty to be set\n")os.Exit(1)}if a.folder == "" && a.file == "" {fmt.Fprintf(os.Stderr, "Error: missing required flag: --folder or --file\n\n")printClassifyUsage() - replacement in cmd/calls_classify.go at line 200
if len(cfg.Classify.Bindings) == 0 {fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.bindings\" (need at least one key)\n", cfgPath)if a.night && a.day {fmt.Fprintf(os.Stderr, "Error: --night and --day are mutually exclusive\n\n")printClassifyUsage()os.Exit(1)}if (a.night || a.day) && (!a.latSet || !a.lngSet) {fmt.Fprintf(os.Stderr, "Error: --night/--day requires both --lat and --lng\n\n")printClassifyUsage() - edit in cmd/calls_classify.go at line 210
} - edit in cmd/calls_classify.go at line 212
// validateBindings checks config bindings and secondary_bindings, returning// the converted []tools.KeyBinding slice. Exits on validation errors.func validateBindings(cfg *utils.Config, cfgPath string) []tools.KeyBinding { - edit in cmd/calls_classify.go at line 260
}return bindings}// RunCallsClassify handles the "calls classify" subcommandfunc RunCallsClassify(args []string) {a := parseClassifyArgs(args)a.validate()// Load reviewer, bindings, and display flags from ~/.skraak/config.json.cfg, cfgPath, err := utils.LoadConfig()if err != nil {fmt.Fprintf(os.Stderr, "Error: %v\n", err)fmt.Fprintf(os.Stderr, "Create %s with a \"classify\" section; run `skraak calls classify --help` for an example.\n", cfgPath)os.Exit(1) - edit in cmd/calls_classify.go at line 278
// Validate config contentsif cfg.Classify.Reviewer == "" {fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.reviewer\"\n", cfgPath)os.Exit(1)}if len(cfg.Classify.Bindings) == 0 {fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.bindings\" (need at least one key)\n", cfgPath)os.Exit(1)}bindings := validateBindings(&cfg, cfgPath) - replacement in cmd/calls_classify.go at line 291
speciesName, callType := utils.ParseSpeciesCallType(species)speciesName, callType := utils.ParseSpeciesCallType(a.species) - replacement in cmd/calls_classify.go at line 295
Folder: folder,File: file,Filter: filter,Folder: a.folder,File: a.file,Filter: a.filter, - replacement in cmd/calls_classify.go at line 300
Certainty: certainty,Sample: sample,Goto: gotoFile,Certainty: a.certainty,Sample: a.sample,Goto: a.gotoFile, - replacement in cmd/calls_classify.go at line 310
Night: night,Day: day,Lat: lat,Lng: lng,Timezone: timezone,Night: a.night,Day: a.day,Lat: a.lat,Lng: a.lng,Timezone: a.timezone, - replacement in Makefile at line 4
.PHONY: test count unit shell.PHONY: test count unit shell goreportcard - replacement in Makefile at line 21
test: count unit shell[3.476]goreportcard:goreportcard-clitest: count unit shell goreportcard - file addition: LICENCE.txt[7.1]
The MIT License (MIT)Copyright © 2026 David CaryPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.