package utils import ( "testing" "time" ) func TestParseLatLng(t *testing.T) { if _, _, ok := ParseLatLng("", ""); ok { t.Error("blank must not parse") } if _, _, ok := ParseLatLng("0", "0"); ok { t.Error("0,0 is an empty map picker, not a place") } if _, _, ok := ParseLatLng("91", "10"); ok { t.Error("out of range") } lat, lng, ok := ParseLatLng(" 11.0168 ", "76.9558") if !ok || lat != 11.0168 || lng != 76.9558 { t.Errorf("got %v %v %v", lat, lng, ok) } } func TestHaversineKm(t *testing.T) { // Coimbatore railway station to Peelamedu, roughly 8 km. d := HaversineKm(11.0018, 76.9660, 11.0290, 77.0290) if d < 7 || d > 9 { t.Errorf("got %.2f km", d) } if HaversineKm(1, 1, 1, 1) != 0 { t.Error("same point") } } func TestOpenNow(t *testing.T) { at := func(h, m int) time.Time { return time.Date(2026, 9, 15, h, m, 0, 0, time.UTC) } if !OpenNow("09:00", "21:00", at(12, 0)) || OpenNow("09:00", "21:00", at(22, 0)) { t.Error("plain hours") } if !OpenNow("20:00", "02:00", at(1, 0)) || OpenNow("20:00", "02:00", at(12, 0)) { t.Error("past midnight") } if !OpenNow("9:00 AM", "9:30 PM", at(21, 0)) || OpenNow("9:00 AM", "9:30 PM", at(21, 45)) { t.Error("12-hour clock") } if !OpenNow("", "", at(3, 0)) || !OpenNow("always", "", at(3, 0)) { t.Error("unknown hours mean open") } } func TestSearchTokens(t *testing.T) { got := SearchTokens("Milk Bikis 100g, Britannia (2 x 50gm)") want := []string{"milk", "bikis", "britannia"} if len(got) != len(want) { t.Fatalf("got %v want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("got %v want %v", got, want) } } if len(SearchTokens("500ml 1kg 2")) != 0 { t.Error("pack sizes alone are not searchable") } } // A single letter is often the whole product name in this catalogue, so it // survives when it follows a word — but not when it stands alone, and not // when it is the multiplier in a pack size. func TestSearchTokensKeepsALetterThatFollowsAWord(t *testing.T) { for _, tc := range []struct { label string want []string }{ {"Parle G", []string{"parle", "g"}}, {"Parle-G", []string{"parle", "g"}}, {"Special K Original", []string{"special", "k", "original"}}, {"G", nil}, // a letter alone is not a search {"2 x 50gm", nil}, // multiplier and pack size, no words {"Milk Bikis 100g, Britannia (2 x 50gm)", []string{"milk", "bikis", "britannia"}}, } { got := SearchTokens(tc.label) if len(got) != len(tc.want) { t.Fatalf("%q: got %v want %v", tc.label, got, tc.want) } for i := range tc.want { if got[i] != tc.want[i] { t.Fatalf("%q: got %v want %v", tc.label, got, tc.want) } } } } func TestFoldAndTightenSeparators(t *testing.T) { if got := FoldSeparators("Parle-G Original"); got != "parle g original" { t.Fatalf("fold: got %q", got) } if FoldSeparators("Parle-G") != FoldSeparators("Parle G") { t.Error("a hyphen and a space are the same separator to us") } if got := TightenLabel("Parle-G"); got != "parleg" { t.Fatalf("tighten: got %q", got) } } // Lens reads the wrapper as well as the product. No catalogue name carries // "bottle" or "pack", so requiring them found no Dettol at all. func TestSearchTokensDropsPackagingWords(t *testing.T) { for _, tc := range []struct { label string want []string }{ {"Dettol bottle pack", []string{"dettol"}}, {"Parle G biscuit pack", []string{"parle", "g", "biscuit"}}, {"Milk Bikis pack", []string{"milk", "bikis"}}, {"Nescafe jar 50g", []string{"nescafe"}}, {"pack", []string{"pack"}}, // nothing else: the wrapper is the search {"combo pack", []string{"combo", "pack"}}, // ditto, both kept } { got := SearchTokens(tc.label) if len(got) != len(tc.want) { t.Fatalf("%q: got %v want %v", tc.label, got, tc.want) } for i := range tc.want { if got[i] != tc.want[i] { t.Fatalf("%q: got %v want %v", tc.label, got, tc.want) } } } }