feat(eisenhower): add ParseEisenhower function

Amolith created

Parses strings like "do-now", "delegate", or numeric "0"-"4" to
Eisenhower quadrants. Adds ErrInvalidEisenhower sentinel error.

Assisted-by: Claude Opus 4.5 via Amp

Change summary

eisenhower.go      | 32 +++++++++++++++++++++
eisenhower_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 100 insertions(+), 1 deletion(-)

Detailed changes

eisenhower.go 🔗

@@ -4,7 +4,11 @@
 
 package lunatask
 
-import "fmt"
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
 
 // Eisenhower represents a quadrant in the Eisenhower priority matrix, which
 // categorizes tasks by whether they are urgent (time-sensitive) and/or
@@ -77,3 +81,29 @@ func (e Eisenhower) IsUrgent() bool {
 func (e Eisenhower) IsImportant() bool {
 	return e == EisenhowerDoNow || e == EisenhowerDoLater
 }
+
+// Errors returned by Eisenhower operations.
+var (
+	// ErrInvalidEisenhower is returned when parsing an unknown Eisenhower quadrant string.
+	ErrInvalidEisenhower = errors.New("invalid eisenhower quadrant")
+)
+
+// ParseEisenhower parses a string to an Eisenhower quadrant value (case-insensitive).
+// Valid values: "uncategorized", "do-now", "delegate", "do-later", "eliminate",
+// as well as numeric strings "0" through "4".
+func ParseEisenhower(str string) (Eisenhower, error) {
+	switch strings.ToLower(str) {
+	case "uncategorized", "0":
+		return EisenhowerUncategorized, nil
+	case "do-now", "donow", "1":
+		return EisenhowerDoNow, nil
+	case "delegate", "2":
+		return EisenhowerDelegate, nil
+	case "do-later", "dolater", "3":
+		return EisenhowerDoLater, nil
+	case "eliminate", "4":
+		return EisenhowerEliminate, nil
+	default:
+		return 0, fmt.Errorf("%w: %q", ErrInvalidEisenhower, str)
+	}
+}

eisenhower_test.go 🔗

@@ -5,6 +5,7 @@
 package lunatask_test
 
 import (
+	"errors"
 	"testing"
 
 	lunatask "git.secluded.site/go-lunatask"
@@ -378,3 +379,71 @@ func TestTaskUpdateBuilder_WithEisenhowerTyped(t *testing.T) {
 
 	assertBodyFieldFloat(t, capture.Body, "eisenhower", 2)
 }
+
+// --- ParseEisenhower ---
+
+func TestParseEisenhower(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		input   string
+		want    lunatask.Eisenhower
+		wantErr bool
+	}{
+		// Text values
+		{"uncategorized_lower", "uncategorized", lunatask.EisenhowerUncategorized, false},
+		{"uncategorized_upper", "UNCATEGORIZED", lunatask.EisenhowerUncategorized, false},
+		{"do_now_lower", "do-now", lunatask.EisenhowerDoNow, false},
+		{"do_now_upper", "DO-NOW", lunatask.EisenhowerDoNow, false},
+		{"donow_lower", "donow", lunatask.EisenhowerDoNow, false},
+		{"donow_upper", "DONOW", lunatask.EisenhowerDoNow, false},
+		{"delegate_lower", "delegate", lunatask.EisenhowerDelegate, false},
+		{"delegate_upper", "DELEGATE", lunatask.EisenhowerDelegate, false},
+		{"do_later_lower", "do-later", lunatask.EisenhowerDoLater, false},
+		{"do_later_upper", "DO-LATER", lunatask.EisenhowerDoLater, false},
+		{"dolater_lower", "dolater", lunatask.EisenhowerDoLater, false},
+		{"dolater_upper", "DOLATER", lunatask.EisenhowerDoLater, false},
+		{"eliminate_lower", "eliminate", lunatask.EisenhowerEliminate, false},
+		{"eliminate_upper", "ELIMINATE", lunatask.EisenhowerEliminate, false},
+
+		// Numeric values
+		{"zero", "0", lunatask.EisenhowerUncategorized, false},
+		{"one", "1", lunatask.EisenhowerDoNow, false},
+		{"two", "2", lunatask.EisenhowerDelegate, false},
+		{"three", "3", lunatask.EisenhowerDoLater, false},
+		{"four", "4", lunatask.EisenhowerEliminate, false},
+
+		// Invalid values
+		{"invalid", "invalid", 0, true},
+		{"empty", "", 0, true},
+		{"five", "5", 0, true},
+		{"negative", "-1", 0, true},
+		{"typo", "elimate", 0, true},
+	}
+
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Parallel()
+
+			got, err := lunatask.ParseEisenhower(testCase.input)
+			if (err != nil) != testCase.wantErr {
+				t.Errorf("ParseEisenhower(%q) error = %v, wantErr %v", testCase.input, err, testCase.wantErr)
+
+				return
+			}
+
+			if testCase.wantErr {
+				if !errors.Is(err, lunatask.ErrInvalidEisenhower) {
+					t.Errorf("ParseEisenhower(%q) error = %v, want ErrInvalidEisenhower", testCase.input, err)
+				}
+
+				return
+			}
+
+			if got != testCase.want {
+				t.Errorf("ParseEisenhower(%q) = %v, want %v", testCase.input, got, testCase.want)
+			}
+		})
+	}
+}