Skip to content

Commit d72f921

Browse files
authored
fix: include unit in Timer.RenderDisplay() output (#10)
Timer syntax like ~{10-15%seconds} was only displaying '10-15' instead of '10-15 seconds' because: 1. The Unit field from the parser wasn't being set on the Timer struct when converting from parser.Component to cooklang.Timer 2. Timer.RenderDisplay() only returned the Duration, ignoring the Unit This fix: - Sets Timer.Unit from component.Unit during parsing in ToCooklangRecipe() - Updates RenderDisplay() to include the unit when available - Adds comprehensive tests for timer parsing and display
1 parent 64adfe4 commit d72f921

2 files changed

Lines changed: 219 additions & 2 deletions

File tree

cooklang.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,21 @@ func (t Timer) Render() string {
281281
}
282282

283283
// RenderDisplay returns timer in plain text format suitable for display.
284-
// Returns the timer name if available, otherwise the duration.
284+
// Returns the duration with unit if available, or just the duration.
285+
// If the timer has a name but no duration, returns the name.
285286
func (t Timer) RenderDisplay() string {
287+
// If there's a duration, show it (optionally with unit)
288+
if t.Duration != "" {
289+
if t.Unit != "" {
290+
return t.Duration + " " + t.Unit
291+
}
292+
return t.Duration
293+
}
294+
// Fall back to name if no duration
286295
if t.Name != "" {
287296
return t.Name
288297
}
289-
return t.Duration
298+
return ""
290299
}
291300

292301
// Render returns the Cooklang syntax representation of this cookware.
@@ -675,6 +684,7 @@ func ToCooklangRecipe(pRecipe *parser.Recipe) *Recipe {
675684
case "timer":
676685
stepComp = &Timer{
677686
Duration: component.Quantity,
687+
Unit: component.Unit,
678688
Name: component.Name,
679689
Annotation: component.Value,
680690
}

timer_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package cooklang
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestTimerRenderDisplay(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
timer Timer
11+
expected string
12+
}{
13+
{
14+
name: "duration with unit",
15+
timer: Timer{
16+
Duration: "10",
17+
Unit: "minutes",
18+
},
19+
expected: "10 minutes",
20+
},
21+
{
22+
name: "duration range with unit",
23+
timer: Timer{
24+
Duration: "10-15",
25+
Unit: "seconds",
26+
},
27+
expected: "10-15 seconds",
28+
},
29+
{
30+
name: "duration without unit",
31+
timer: Timer{
32+
Duration: "5",
33+
},
34+
expected: "5",
35+
},
36+
{
37+
name: "named timer without duration falls back to name",
38+
timer: Timer{
39+
Name: "resting",
40+
},
41+
expected: "resting",
42+
},
43+
{
44+
name: "duration takes precedence over name",
45+
timer: Timer{
46+
Duration: "10",
47+
Unit: "minutes",
48+
Name: "boiling",
49+
},
50+
expected: "10 minutes",
51+
},
52+
{
53+
name: "empty timer returns empty string",
54+
timer: Timer{},
55+
expected: "",
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
result := tt.timer.RenderDisplay()
62+
if result != tt.expected {
63+
t.Errorf("RenderDisplay() = %q, want %q", result, tt.expected)
64+
}
65+
})
66+
}
67+
}
68+
69+
func TestTimerParsingWithUnit(t *testing.T) {
70+
tests := []struct {
71+
name string
72+
input string
73+
expectedDuration string
74+
expectedUnit string
75+
expectedDisplay string
76+
}{
77+
{
78+
name: "simple timer with minutes",
79+
input: "Boil for ~{10%minutes}.",
80+
expectedDuration: "10",
81+
expectedUnit: "minutes",
82+
expectedDisplay: "10 minutes",
83+
},
84+
{
85+
name: "timer with seconds",
86+
input: "Stir for ~{30%seconds}.",
87+
expectedDuration: "30",
88+
expectedUnit: "seconds",
89+
expectedDisplay: "30 seconds",
90+
},
91+
{
92+
name: "timer with range and unit",
93+
input: "Mix for ~{10-15%seconds}.",
94+
expectedDuration: "10-15",
95+
expectedUnit: "seconds",
96+
expectedDisplay: "10-15 seconds",
97+
},
98+
{
99+
name: "timer with hours",
100+
input: "Bake for ~{2%hours}.",
101+
expectedDuration: "2",
102+
expectedUnit: "hours",
103+
expectedDisplay: "2 hours",
104+
},
105+
{
106+
name: "timer without unit",
107+
input: "Wait ~{5}.",
108+
expectedDuration: "5",
109+
expectedUnit: "",
110+
expectedDisplay: "5",
111+
},
112+
{
113+
name: "named timer with unit",
114+
input: "Let it ~rest{10%minutes}.",
115+
expectedDuration: "10",
116+
expectedUnit: "minutes",
117+
expectedDisplay: "10 minutes",
118+
},
119+
}
120+
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
recipe, err := ParseString(tt.input)
124+
if err != nil {
125+
t.Fatalf("Failed to parse recipe: %v", err)
126+
}
127+
128+
// Find the timer in the parsed recipe
129+
var foundTimer *Timer
130+
step := recipe.FirstStep
131+
for step != nil && foundTimer == nil {
132+
component := step.FirstComponent
133+
for component != nil {
134+
if timer, ok := component.(*Timer); ok {
135+
foundTimer = timer
136+
break
137+
}
138+
component = component.GetNext()
139+
}
140+
step = step.NextStep
141+
}
142+
143+
if foundTimer == nil {
144+
t.Fatal("No timer found in parsed recipe")
145+
}
146+
147+
if foundTimer.Duration != tt.expectedDuration {
148+
t.Errorf("Duration = %q, want %q", foundTimer.Duration, tt.expectedDuration)
149+
}
150+
151+
if foundTimer.Unit != tt.expectedUnit {
152+
t.Errorf("Unit = %q, want %q", foundTimer.Unit, tt.expectedUnit)
153+
}
154+
155+
display := foundTimer.RenderDisplay()
156+
if display != tt.expectedDisplay {
157+
t.Errorf("RenderDisplay() = %q, want %q", display, tt.expectedDisplay)
158+
}
159+
})
160+
}
161+
}
162+
163+
func TestTimerInRecipeSteps(t *testing.T) {
164+
// Test that timers are correctly rendered in recipe step text
165+
recipe, err := ParseString("Stir for ~{10-15%seconds}.")
166+
if err != nil {
167+
t.Fatalf("Failed to parse recipe: %v", err)
168+
}
169+
170+
// Get the step instruction text
171+
step := recipe.FirstStep
172+
if step == nil {
173+
t.Fatal("No steps found in recipe")
174+
}
175+
176+
// Build the instruction text from components
177+
var instructionText string
178+
component := step.FirstComponent
179+
for component != nil {
180+
switch c := component.(type) {
181+
case *Timer:
182+
instructionText += c.RenderDisplay()
183+
case *Instruction:
184+
instructionText += c.Text
185+
}
186+
component = component.GetNext()
187+
}
188+
189+
// The instruction should contain "10-15 seconds"
190+
expectedSubstring := "10-15 seconds"
191+
if !contains(instructionText, expectedSubstring) {
192+
t.Errorf("Instruction text %q should contain %q", instructionText, expectedSubstring)
193+
}
194+
}
195+
196+
func contains(s, substr string) bool {
197+
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
198+
}
199+
200+
func containsHelper(s, substr string) bool {
201+
for i := 0; i <= len(s)-len(substr); i++ {
202+
if s[i:i+len(substr)] == substr {
203+
return true
204+
}
205+
}
206+
return false
207+
}

0 commit comments

Comments
 (0)