cyclomatic complexity

quietlight
May 4, 2026, 3:06 AM
NS4TDPLNAWJYJN37PZDYXMG6OJSAWZCMTPSPKX73JCLZZAMY25BAC

Dependencies

  • [2] LQLC7S3A trying gemini: Inconsistent Standards in @utils/ refactoring
  • [3] EA6OQXWF added makefile to count loc and run shell scripts
  • [4] 54GPBNIX added +_ for tui to select segments with no calltype
  • [5] KZKLAINJ run out of space on nest, cleaned out
  • [*] SJN7IKIV

Change contents

  • replacement in utils/wav_metadata.go at line 417
    [2.3450][2.3450:3558]()
    startOffset = startSample * int64(blockAlign)
    if startOffset > dataSize {
    startOffset = dataSize
    }
    [2.3450]
    [4.39298]
    startOffset = min(startSample*int64(blockAlign), dataSize)
  • replacement in utils/wav_metadata.go at line 422
    [2.3627][2.3627:3728]()
    endOffset := endSample * int64(blockAlign)
    if endOffset > dataSize {
    endOffset = dataSize
    }
    [2.3627]
    [2.3728]
    endOffset := min(endSample*int64(blockAlign), dataSize)
  • edit in utils/filename_parser_test.go at line 5
    [4.99021]
    [4.99021]
    "time"
  • edit in utils/filename_parser_test.go at line 8
    [4.99024]
    [4.99024]
    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
    [4.99552][4.99552:100784]()
    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 { // October
    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 { // November
    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())
    }
    [4.99552]
    [4.100784]
    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
    [4.101343][4.101343:102167]()
    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 { // October
    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 { // December
    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())
    }
    [4.101343]
    [4.102167]
    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
    [4.102540][4.102540:103410]()
    if results[0].Timestamp.Year() != 2023 {
    t.Errorf("Year incorrect: got %d, want 2023", results[0].Timestamp.Year())
    }
    if results[0].Timestamp.Month() != 6 { // June
    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())
    }
    [4.102540]
    [4.103410]
    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
    [4.104080][4.104080:105032]()
    if results[0].Timestamp.Day() != 12 {
    t.Errorf("Day incorrect: got %d, want 12", results[0].Timestamp.Day())
    }
    if results[0].Timestamp.Month() != 1 { // January
    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 { // March
    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)
    }
    [4.104080]
    [4.105032]
    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
    [4.105037][4.105037:105285]()
    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
    [4.105680][4.105680:107175]()
    if results[0].Timestamp.Year() != 2023 {
    t.Errorf("Year incorrect: got %d, want 2023", results[0].Timestamp.Year())
    }
    if results[0].Timestamp.Month() != 6 { // June
    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 { // November
    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())
    }
    [4.105680]
    [4.107175]
    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
    [4.107782][4.107782:108531]()
    if results[0].Timestamp.Day() != 18 {
    t.Errorf("Day incorrect: got %d, want 18", results[0].Timestamp.Day())
    }
    if results[0].Timestamp.Month() != 1 { // January
    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())
    }
    [4.107782]
    [4.108531]
    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
    [4.108532][4.108532:108646]()
    if results[1].Timestamp.Day() != 12 {
    t.Errorf("Day incorrect: got %d, want 12", results[1].Timestamp.Day())
    [4.108532]
    [4.108646]
    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
    [4.108650][4.108650:108779]()
    if results[1].Timestamp.Month() != 1 { // January
    t.Errorf("Month incorrect: got %d, want 1", results[1].Timestamp.Month())
    [4.108650]
    [4.108779]
    if err != nil && err.Error() != "no filenames provided" {
    t.Logf("Error message: %v", err)
  • replacement in utils/filename_parser_test.go at line 204
    [4.108783][4.108783:108908]()
    if results[1].Timestamp.Year() != 2019 {
    t.Errorf("Year incorrect: got %d, want 2019", results[1].Timestamp.Year())
    }
    [4.108783]
    [4.108908]
    })
  • replacement in utils/filename_parser_test.go at line 206
    [4.108909][4.108909:109023]()
    if results[4].Timestamp.Day() != 31 {
    t.Errorf("Day incorrect: got %d, want 31", results[4].Timestamp.Day())
    [4.108909]
    [4.109023]
    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
    [4.109027][4.109027:109283]()
    if results[4].Timestamp.Month() != 3 { // March
    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())
    }
  • replacement in utils/filename_parser_test.go at line 240
    [4.110277][4.110277:110605]()
    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)
    }
    [4.110277]
    [4.110605]
    results := parseAndApply(t, []string{"201012_123456.wav", "201014_123456.WAV"}, "UTC")
  • replacement in utils/filename_parser_test.go at line 244
    [4.110690][4.110690:110838]()
    // Check timezone offset is +00:00
    _, offset := results[0].Zone()
    if offset != 0 {
    t.Errorf("UTC offset should be 0, got %d", offset)
    }
    [4.110690]
    [4.110838]
    assertOffset(t, results[0], 0)
  • replacement in utils/filename_parser_test.go at line 248
    [4.110941][4.110941:111151]()
    // 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)
    [4.110941]
    [4.111151]
    // 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
    [4.111236][4.111236:111454]()
    "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)
    }
    [4.111236]
    [4.111454]
    "20210420_120000.wav", // April 20th - Standard time
    }, "Pacific/Auckland")
  • edit in utils/filename_parser_test.go at line 255
    [4.111455][4.111455:111593]()
    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
    [4.111679][4.111679:111871]()
    // 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
    [4.111679]
    [4.111871]
    // 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
    [4.111876][4.111876:112745]()
    // Check all offsets are the same
    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)
    }
    }
    // The offset should be UTC+13 (from the earliest file: April 1st)
    expectedOffsetSeconds := 13 * 3600
    if 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 day
    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())
    }
    }
    [4.111876]
    [4.112745]
    // All at 12:00 local - 13h = 23:00 UTC previous day
    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})
  • replacement in utils/filename_parser_test.go at line 271
    [4.112828][4.112828:112935]()
    // Files not in chronological order - should still use earliest file for offset
    filenames := []string{
    [4.112828]
    [4.112935]
    results := parseAndApply(t, []string{
  • replacement in utils/filename_parser_test.go at line 273
    [4.112983][4.112983:113059]()
    "20210401_120000.wav", // April 1st (earliest - should determine offset)
    [4.112983]
    [4.113059]
    "20210401_120000.wav", // April 1st (earliest - determines offset)
  • replacement in utils/filename_parser_test.go at line 275
    [4.113107][4.113107:113374]()
    }
    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)
    }
    [4.113107]
    [4.113374]
    }, "Pacific/Auckland")
  • replacement in utils/filename_parser_test.go at line 277
    [4.113375][4.113375:113655]()
    // All files should use UTC+13 offset (from April 1st, the earliest)
    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)
    }
    [4.113375]
    [4.113655]
    // 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
    [4.113660][4.113660:114036]()
    // Results should maintain original filename order
    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())
    }
    [4.113660]
    [4.114036]
    // Results maintain original filename order
    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})
  • replacement in utils/filename_parser_test.go at line 289
    [4.114135][4.114135:114454]()
    // Test files spanning multiple months with different DST periods
    filenames := []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)
    }
    [4.114135]
    [4.114454]
    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
    [4.114455][4.114455:115002]()
    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 * 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)
    }
    [4.114455]
    [4.115002]
    // All files use offset from earliest (February): UTC+13
    for _, r := range results {
    assertOffset(t, r, 13*3600)
  • replacement in utils/filename_parser_test.go at line 300
    [4.115007][4.115007:115265]()
    // Verify UTC conversion is consistent with fixed offset
    for 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())
    }
    }
    [4.115007]
    [4.115265]
    // 12:00 local - 13h = 23:00 UTC previous day
    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})
  • replacement in utils/filename_parser_test.go at line 307
    [4.115352][4.115352:115429]()
    // Test US spring DST transition (March 14, 2021)
    filenames := []string{
    [4.115352]
    [4.115429]
    results := parseAndApply(t, []string{
  • replacement in utils/filename_parser_test.go at line 309
    [4.115490][4.115490:115703]()
    "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)
    }
    [4.115490]
    [4.115703]
    "20210320_120000.wav", // March 20th - after DST (would be UTC-4)
    }, "America/New_York")
  • replacement in utils/filename_parser_test.go at line 312
    [4.115704][4.115704:115837]()
    results, err := ApplyTimezoneOffset(parsed, "America/New_York")
    if err != nil {
    t.Fatalf("Failed to apply timezone: %v", err)
    [4.115704]
    [4.115837]
    // All files use offset from earliest (March 10th): UTC-5
    for _, r := range results {
    assertOffset(t, r, -5*3600)
  • replacement in utils/filename_parser_test.go at line 317
    [4.115842][4.115842:116346]()
    // All files should use the same offset from earliest file (March 10th)
    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)
    }
    }
    // Verify UTC conversion uses fixed offset
    for i, r := range results {
    utc := r.UTC()
    if utc.Hour() != 17 { // 12 + 5 = 17
    t.Errorf("File %d UTC hour incorrect: got %d, want 17", i, utc.Hour())
    }
    }
    [4.115842]
    [4.116346]
    // 12:00 local + 5h = 17:00 UTC
    assertTimestamp(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
    [4.119335][4.119335:120263]()
    // 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 day
    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())
    }
    [4.119335]
    [4.120263]
    // 21:00 Pacific/Auckland (May = UTC+12) → 09:00 UTC same day
    results := 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
    [4.120346][4.120346:121015]()
    // 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())
    }
    [4.120346]
    [4.121015]
    // 12:00 Pacific/Auckland (May = UTC+12) → 00:00 UTC same day
    results := 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
    [4.121089][4.121089:121758]()
    // Test early morning: 02:00 Pacific/Auckland
    // Should become 14:00 UTC previous day
    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())
    }
    [4.121089]
    [4.121758]
    // 02:00 Pacific/Auckland (May = UTC+12) → 14:00 UTC previous day
    results := 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
    [4.121848][4.121848:122498]()
    // Test 15:00 (3 PM) New York in June (UTC-4 during DST)
    // Should become 19:00 UTC same day
    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())
    }
    [4.121848]
    [4.122498]
    // 15:00 New York (June = UTC-4 during DST) → 19:00 UTC same day
    results := 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
    [4.1145180][4.1145180:1145436]()
    // RunCallsClassify handles the "calls classify" subcommand
    func RunCallsClassify(args []string) {
    var folder, file, filter, species, gotoFile, timezone string
    var certainty, sample int
    var night, day bool
    var lat, lng float64
    var latSet, lngSet bool
    [4.1145180]
    [4.1145436]
    // classifyArgs holds parsed CLI arguments for the classify subcommand.
    type classifyArgs struct {
    folder string
    file string
    filter string
    species string
    gotoFile string
    timezone string
    certainty int
    sample int
    night bool
    day bool
    lat float64
    lng float64
    latSet bool
    lngSet bool
    }
  • replacement in cmd/calls_classify.go at line 84
    [4.1145437][4.1145437:1145510]()
    // Default to -1 (no filter / no sampling)
    certainty = -1
    sample = -1
    [4.1145437]
    [4.1145510]
    // 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
    [4.1145511][4.1145511:1145531]()
    // Parse arguments
  • replacement in cmd/calls_classify.go at line 95
    [4.1145612][4.1145612:1145744]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --folder requires a value\n")
    os.Exit(1)
    }
    folder = args[i+1]
    [4.1145612]
    [4.1145744]
    a.folder = a.requireValue(args, i, "--folder")
  • edit in cmd/calls_classify.go at line 97
    [4.1145754][4.1145754:1145755]()
  • replacement in cmd/calls_classify.go at line 98
    [4.1145772][4.1145772:1145900]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --file requires a value\n")
    os.Exit(1)
    }
    file = args[i+1]
    [4.1145772]
    [4.1145900]
    a.file = a.requireValue(args, i, "--file")
  • edit in cmd/calls_classify.go at line 100
    [4.1145910][4.1145910:1145911]()
  • replacement in cmd/calls_classify.go at line 101
    [4.1145930][4.1145930:1146178]()
    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]
    [4.1145930]
    [4.1146178]
    a.filter = a.requireUniqueValue(args, i, "--filter", a.filter)
  • edit in cmd/calls_classify.go at line 103
    [4.1146188][4.1146188:1146189]()
  • replacement in cmd/calls_classify.go at line 104
    [4.1146209][4.1146209:1146461]()
    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]
    [4.1146209]
    [4.1146461]
    a.species = a.requireUniqueValue(args, i, "--species", a.species)
  • edit in cmd/calls_classify.go at line 106
    [4.1146471][4.1146471:1146472]()
  • replacement in cmd/calls_classify.go at line 107
    [4.1146494][4.1146494:1146892]()
    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 = v
    [4.1146494]
    [4.1146892]
    a.certainty = a.requireIntRange(args, i, "--certainty", 0, 100)
  • replacement in cmd/calls_classify.go at line 109
    [4.1146902][4.1146902:1147282]()
    case "--night":
    night = true
    i++
    case "--day":
    day = true
    i++
    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 = v
    latSet = true
    [4.1146902]
    [4.1147282]
    case "--sample":
    a.sample = a.requireIntRange(args, i, "--sample", 1, 100)
  • replacement in cmd/calls_classify.go at line 112
    [4.1147292][4.1147292:1147592]()
    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 = v
    lngSet = true
    [4.1147292]
    [4.1147592]
    case "--goto":
    a.gotoFile = a.requireValue(args, i, "--goto")
  • edit in cmd/calls_classify.go at line 115
    [4.1147602][4.1147602:1147603]()
  • replacement in cmd/calls_classify.go at line 116
    [4.1147624][4.1147624:1147760]()
    if i+1 >= len(args) {
    fmt.Fprintf(os.Stderr, "Error: --timezone requires a value\n")
    os.Exit(1)
    }
    timezone = args[i+1]
    [4.1147624]
    [4.1147760]
    a.timezone = a.requireValue(args, i, "--timezone")
    i += 2
    case "--lat":
    a.lat = a.requireFloat(args, i, "--lat")
    a.latSet = true
    i += 2
    case "--lng":
    a.lng = a.requireFloat(args, i, "--lng")
    a.lngSet = true
  • replacement in cmd/calls_classify.go at line 126
    [4.1147770][4.1147770:1147771]()
    [4.1147770]
    [4.1147771]
    case "--night":
    a.night = true
    i++
    case "--day":
    a.day = true
    i++
  • edit in cmd/calls_classify.go at line 135
    [4.1147832][4.1147832:1148410]()
    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 = v
    i += 2
    case "--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
    [4.1148529][4.1148529:1148720]()
    // --sample 1-99 requires --certainty; --sample 100 is a no-op
    if sample > 0 && sample < 100 && certainty < 0 {
    fmt.Fprintf(os.Stderr, "Error: --sample requires --certainty to be set\n")
    [4.1148529]
    [4.1148720]
    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
    [4.1148736]
    [4.1148736]
    return args[i+1]
    }
  • replacement in cmd/calls_classify.go at line 154
    [4.1148737][4.1148737:1148902]()
    // Validate required flags
    if folder == "" && file == "" {
    fmt.Fprintf(os.Stderr, "Error: missing required flag: --folder or --file\n\n")
    printClassifyUsage()
    [4.1148737]
    [4.1148902]
    // 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
    [4.1148918]
    [4.1148918]
    return a.requireValue(args, i, flag)
    }
  • replacement in cmd/calls_classify.go at line 163
    [4.1148919][4.1148919:1149041]()
    if night && day {
    fmt.Fprintf(os.Stderr, "Error: --night and --day are mutually exclusive\n\n")
    printClassifyUsage()
    [4.1148919]
    [4.1149041]
    // 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
    [4.1149057][4.1149057:1149208]()
    if (night || day) && (!latSet || !lngSet) {
    fmt.Fprintf(os.Stderr, "Error: --night/--day requires both --lat and --lng\n\n")
    printClassifyUsage()
    [4.1149057]
    [4.1149208]
    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
    [4.1149224]
    [4.1149224]
    return v
    }
  • replacement in cmd/calls_classify.go at line 178
    [4.1149225][4.1149225:1149341]()
    // Load reviewer, bindings, and display flags from ~/.skraak/config.json.
    cfg, cfgPath, err := utils.LoadConfig()
    [4.1149225]
    [4.1149341]
    // 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
    [4.1149358][4.1149358:1149532]()
    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)
    [4.1149358]
    [4.1149532]
    fmt.Fprintf(os.Stderr, "Error: %s must be a number\n", flag)
  • edit in cmd/calls_classify.go at line 186
    [4.1149548]
    [4.1149548]
    return v
    }
  • replacement in cmd/calls_classify.go at line 189
    [4.1149549][4.1149549:1149694]()
    // Validate config contents
    if cfg.Classify.Reviewer == "" {
    fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.reviewer\"\n", cfgPath)
    [4.1149549]
    [4.1149694]
    // 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
    [4.1149710][4.1149710:1149854]()
    if len(cfg.Classify.Bindings) == 0 {
    fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.bindings\" (need at least one key)\n", cfgPath)
    [4.1149710]
    [4.1149854]
    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
    [4.1149870]
    [4.1149870]
    }
  • edit in cmd/calls_classify.go at line 212
    [4.1149871]
    [4.1149871]
    // 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
    [4.1151534]
    [4.1151534]
    }
    return bindings
    }
    // RunCallsClassify handles the "calls classify" subcommand
    func 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
    [4.1151538]
    [4.1151538]
    // Validate config contents
    if 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
    [4.1151565][4.1151565:1151627]()
    speciesName, callType := utils.ParseSpeciesCallType(species)
    [4.1151565]
    [4.1151627]
    speciesName, callType := utils.ParseSpeciesCallType(a.species)
  • replacement in cmd/calls_classify.go at line 295
    [4.1151678][4.1151678:1151763]()
    Folder: folder,
    File: file,
    Filter: filter,
    [4.1151678]
    [4.1151763]
    Folder: a.folder,
    File: a.file,
    Filter: a.filter,
  • replacement in cmd/calls_classify.go at line 300
    [4.1151828][4.1151828:1151920]()
    Certainty: certainty,
    Sample: sample,
    Goto: gotoFile,
    [4.1151828]
    [4.1151920]
    Certainty: a.certainty,
    Sample: a.sample,
    Goto: a.gotoFile,
  • replacement in cmd/calls_classify.go at line 310
    [4.1152214][4.1152214:1152351]()
    Night: night,
    Day: day,
    Lat: lat,
    Lng: lng,
    Timezone: timezone,
    [4.1152214]
    [4.1152351]
    Night: a.night,
    Day: a.day,
    Lat: a.lat,
    Lng: a.lng,
    Timezone: a.timezone,
  • replacement in Makefile at line 4
    [3.139][3.139:169]()
    .PHONY: test count unit shell
    [3.139]
    [3.169]
    .PHONY: test count unit shell goreportcard
  • replacement in Makefile at line 21
    [3.476][3.476:499]()
    test: count unit shell
    [3.476]
    goreportcard:
    goreportcard-cli
    test: count unit shell goreportcard
  • file addition: LICENCE.txt (----------)
    [7.1]
    The MIT License (MIT)
    Copyright © 2026 David Cary
    Permission 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.