Skip to content

Commit

Permalink
feat(reminder): proper lexer function
Browse files Browse the repository at this point in the history
  • Loading branch information
aldy505 committed Dec 25, 2023
1 parent c05145f commit 4550265
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 16 deletions.
62 changes: 62 additions & 0 deletions reminder/clock_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package reminder

import (
"bufio"
"fmt"
"strconv"
"strings"
)

func ParseClock(s string) (hour int, minute int, err error) {
scanner := bufio.NewScanner(strings.NewReader(s))
scanner.Split(bufio.ScanRunes)

var colonMark bool
var s_hour string
var s_minute string
for scanner.Scan() {
t := scanner.Text()
if t == ":" {
colonMark = true
continue
}

if !colonMark {
s_hour += t
} else {
s_minute += t
}
}

hour, err = strconv.Atoi(s_hour)
if err != nil {
return
}

minute, err = strconv.Atoi(s_minute)
if err != nil {
return
}

if hour > 24 {
err = fmt.Errorf("invalid hour, exceeds 24")
return
}

if hour < 0 {
err = fmt.Errorf("invalid hour, negative number")
return
}

if minute > 60 {
err = fmt.Errorf("invalid minute, exceeds 60")
return
}

if minute < 0 {
err = fmt.Errorf("invalid minute, negative number")
return
}

return
}
78 changes: 78 additions & 0 deletions reminder/clock_parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package reminder_test

import (
"errors"
"github.com/teknologi-umum/captcha/reminder"
"strconv"
"testing"
)

func TestParseClock(t *testing.T) {
testCases := []struct {
name string
input string
expectHour int
expectMinute int
expectError error
}{
{
name: "happy case 1",
input: "00:00",
expectHour: 0,
expectMinute: 0,
expectError: nil,
},
{
name: "happy case 2",
input: "23:59",
expectHour: 23,
expectMinute: 59,
expectError: nil,
},
{
name: "happy case 3",
input: "05:05",
expectHour: 5,
expectMinute: 5,
expectError: nil,
},
{
name: "happy case 4",
input: "20:20",
expectHour: 20,
expectMinute: 20,
expectError: nil,
},
{
name: "hour is not a number",
input: "abc:00",
expectHour: 0,
expectMinute: 0,
expectError: strconv.ErrSyntax,
},
{
name: "minute is not a number",
input: "15:abc",
expectHour: 15,
expectMinute: 0,
expectError: strconv.ErrSyntax,
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
hour, minute, err := reminder.ParseClock(testCase.input)
if hour != testCase.expectHour {
t.Errorf("expecting hour to be %d, got %d", testCase.expectHour, hour)
}

if minute != testCase.expectMinute {
t.Errorf("expecting minute to be %d, got %d", testCase.expectMinute, minute)
}

if !errors.Is(err, testCase.expectError) {
t.Errorf("expecting error to be %v, got %v", testCase.expectError, err)
}
})
}
}
123 changes: 113 additions & 10 deletions reminder/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ package reminder
import (
"bufio"
"bytes"
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"time"
)

type SentenceElement uint8

const (
None SentenceElement = iota
Subject
Time
Verb
TimePreposition
VerbPreposition
Expand All @@ -34,29 +39,33 @@ func ScanSpace(data []byte, atEOF bool) (advance int, token []byte, err error) {
return 0, nil, nil
}

func isNumber(s string) bool {
_, err := strconv.Atoi(s)
return err == nil
}

var verbPreposition = []string{"for", "untuk", "buat", "to", "about", "tentang", "soal", "regarding"}
var timePreposition = []string{"at", "in", "on", "di", "jam", "pada"}
var conjunction = []string{"and", "or", "dan", "&", "atau"}
var validSubjects = []string{"me", "aku", "saya", "gw", "gua", "gue", "gweh"}

var clockRegex = regexp.MustCompile("[0-9]{1,2}:[0-9]{2}")

func ParseText(text string) (Reminder, error) {
scanner := bufio.NewScanner(strings.NewReader(text))
scanner.Split(ScanSpace)

var reminder Reminder
var i = 0
var lastPartCategory SentenceElement
var expectedNextPartCategory SentenceElement
var lastPartCategory SentenceElement = None
var expectedNextPartCategory SentenceElement = None
var partialTimeString string
var appliedVerbPreposition bool
for scanner.Scan() {
part := scanner.Text()
// subject = 0: me, @mention, saya, gw, aku
// predicate = for, in, at, on

switch expectedNextPartCategory {
case Subject:
//goto ValidateSubject
}

if len(reminder.Subject) == 0 {
if len(reminder.Subject) == 0 || expectedNextPartCategory == Subject {
//ValidateSubject:
if len(reminder.Subject) == 3 {
continue
Expand All @@ -67,6 +76,7 @@ func ParseText(text string) (Reminder, error) {
// it is subject indeed
reminder.Subject = append(reminder.Subject, part)
lastPartCategory = Subject
expectedNextPartCategory = None
continue
}

Expand All @@ -75,6 +85,7 @@ func ParseText(text string) (Reminder, error) {
// it is subject, yeah
reminder.Subject = append(reminder.Subject, part)
lastPartCategory = Subject
expectedNextPartCategory = None
continue
}
}
Expand All @@ -88,7 +99,99 @@ func ParseText(text string) (Reminder, error) {
continue
}

i++
// check for preposition on time
if slices.Contains(timePreposition, part) && reminder.Time.IsZero() {
lastPartCategory = TimePreposition
expectedNextPartCategory = Time
continue
}

if lastPartCategory == TimePreposition && expectedNextPartCategory == Time {
// check if it's matches with clock regex
if clockRegex.MatchString(part) {
// parse clock
hour, minute, err := ParseClock(part)
if err != nil {
return Reminder{}, fmt.Errorf("parsing clock: %w", err)
}

now := time.Now()
reminder.Time = time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, time.FixedZone("UTC+7", 7*60*60))
lastPartCategory = Time
expectedNextPartCategory = Verb
continue
}

// if partialTimeString is empty, check if the current part is number
if partialTimeString == "" && isNumber(part) {
partialTimeString = part
continue
} else {
// partial string is not empty
now := time.Now()

// switch case is faster rather than doing slices.Contains like this
//if slices.Contains(timeDuration, part) {}
switch part {
case "minute", "minutes", "menit":
partialTimeString += "m"
duration, err := time.ParseDuration(partialTimeString)
if err != nil {
return reminder, fmt.Errorf("invalid duration: %w", err)
}

// must not exceed 24 hours
if duration > time.Duration(24)*time.Hour {
return reminder, ErrExceeds24Hours
}

reminder.Time = now.Add(duration).
In(time.FixedZone("UTC+7", 7*60*60)).
Round(time.Second)
lastPartCategory = Time
expectedNextPartCategory = Verb
continue
case "hour", "hours", "jam":
partialTimeString += "h"
duration, err := time.ParseDuration(partialTimeString)
if err != nil {
return reminder, fmt.Errorf("invalid duration: %w", err)
}

// must not exceed 24 hours
if duration > time.Duration(24)*time.Hour {
return reminder, ErrExceeds24Hours
}

reminder.Time = now.Add(duration).
In(time.FixedZone("UTC+7", 7*60*60)).
Round(time.Second)
lastPartCategory = Time
expectedNextPartCategory = Verb
continue
}
}
}

if expectedNextPartCategory == None || expectedNextPartCategory == Verb {
if slices.Contains(verbPreposition, part) {
if !appliedVerbPreposition {
appliedVerbPreposition = true
lastPartCategory = VerbPreposition
continue
}
}

reminder.Object += part + " "
}
}

reminder.Object = strings.TrimSpace(reminder.Object)

// validate for time, it should be later than now.
// if it's not, then we should add the day by 1?
if reminder.Time.Unix() < time.Now().Unix() {
reminder.Time = reminder.Time.Add(time.Hour * 24)
}

return reminder, nil
Expand Down
Loading

0 comments on commit 4550265

Please sign in to comment.