From 02a7661d85026c69e2d60fd203116df0b0a07fe1 Mon Sep 17 00:00:00 2001 From: TheEdgeOfRage Date: Mon, 8 Jan 2024 23:33:16 +0100 Subject: [PATCH] Add initial implementation --- cmd/main.go | 38 ++++++++++++++ go.mod | 3 ++ rpn/lexer.go | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++ rpn/parser.go | 39 ++++++++++++++ rpn/rpn.go | 108 ++++++++++++++++++++++++++++++++++++++ rpn/stack.go | 71 +++++++++++++++++++++++++ rpn/token.go | 35 ++++++++++++ 7 files changed, 437 insertions(+) create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 rpn/lexer.go create mode 100644 rpn/parser.go create mode 100644 rpn/rpn.go create mode 100644 rpn/stack.go create mode 100644 rpn/token.go diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..eca8fdc --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + + "gitea.theedgeofrage.com/theedgeofrage/rpn/rpn" +) + +func main() { + rpn := rpn.NewRPN() + reader := bufio.NewReader(os.Stdin) + for { + rpn.PrintStack() + // input := "" + // _, err := fmt.Scanln(&input) + + fmt.Print("> ") + input, err := reader.ReadString('\n') + if err != nil { + if errors.Is(err, io.EOF) { + fmt.Println() + return + } + fmt.Println(err) + continue + } + + err = rpn.Eval(input) + if err != nil { + fmt.Println(err) + continue + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9f07723 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.theedgeofrage.com/theedgeofrage/rpn + +go 1.21.5 diff --git a/rpn/lexer.go b/rpn/lexer.go new file mode 100644 index 0000000..1a01c15 --- /dev/null +++ b/rpn/lexer.go @@ -0,0 +1,143 @@ +package rpn + +import ( + "fmt" + "math" + "strconv" + "unicode" +) + +type Lexer struct { + input *Input +} + +func (l *Lexer) parseNumber() (float64, error) { + numStr := "" + char := l.input.NextChar() + for { + if !unicode.IsNumber(char) && char != '.' { + break + } + + numStr += string(char) + char = l.input.Eat() + } + + num, err := strconv.ParseFloat(numStr, 64) + if err != nil { + return 0, err + } + + return num, nil +} + +func (l *Lexer) parseWord() (*Token, error) { + word := "" + char := l.input.NextChar() + for { + if !unicode.IsLetter(char) { + break + } + + word += string(char) + char = l.input.Eat() + } + + switch word { + case "sqrt": + return &Token{unaryOp, sqrt, 0}, nil + case "dec": + return &Token{dec, 0, 0}, nil + case "bin": + return &Token{bin, 0, 0}, nil + case "hex": + return &Token{hex, 0, 0}, nil + case "pi": + return &Token{number, 0, math.Pi}, nil + case "pop": + return &Token{pop, 0, 0}, nil + case "swap": + return &Token{swap, 0, 0}, nil + case "clr": + return &Token{clr, 0, 0}, nil + case "help": + return &Token{help, 0, 0}, nil + case "exit": + return &Token{exit, 0, 0}, nil + default: + return nil, fmt.Errorf("Unknown input: %s", word) + } +} + +func (l *Lexer) Parse(input string) ([]*Token, error) { + var err error + var num float64 + var token *Token + l.input = NewInput(input) + tokens := []*Token{} + for { + char := l.input.NextChar() + if char == 0 { + break + } + + switch char { + case '+': + l.input.Eat() + token = &Token{binaryOp, plus, 0} + case '-': + char = l.input.Eat() + if unicode.IsNumber(char) { + num, err = l.parseNumber() + token = &Token{number, 0, -num} + if err != nil { + return nil, err + } + } else { + token = &Token{binaryOp, minus, 0} + } + case '*': + l.input.Eat() + token = &Token{binaryOp, multiply, 0} + case '/': + l.input.Eat() + token = &Token{binaryOp, divide, 0} + case '%': + l.input.Eat() + token = &Token{binaryOp, mod, 0} + case '^': + l.input.Eat() + token = &Token{binaryOp, power, 0} + case ' ': + l.input.Eat() + continue + case 0x0a: + l.input.Eat() + continue + case 0x04: // Ctrl-D + token = &Token{exit, 0, 0} + case '?': + l.input.Eat() + token = &Token{help, 0, 0} + default: + if unicode.IsNumber(char) { + num, err = l.parseNumber() + if err != nil { + return nil, err + } + token = &Token{number, 0, num} + } else if unicode.IsLetter(char) { + token, err = l.parseWord() + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("Unknown input: %c", char) + } + } + + tokens = append(tokens, token) + } + + return tokens, nil +} diff --git a/rpn/parser.go b/rpn/parser.go new file mode 100644 index 0000000..b7c02f1 --- /dev/null +++ b/rpn/parser.go @@ -0,0 +1,39 @@ +package rpn + +type Input struct { + characters []rune + nextChar rune +} + +func NewInput(input string) *Input { + characters := []rune(input) + if len(characters) == 0 { + return nil + } + + i := &Input{ + characters: characters, + nextChar: characters[0], + } + return i +} + +// Eat consumes a character from the input and returns the next one. +func (i *Input) Eat() rune { + if len(i.characters) == 0 { + return 0 + } + + i.characters = i.characters[1:] + if len(i.characters) == 0 { + i.nextChar = 0 + } else { + i.nextChar = i.characters[0] + } + + return i.nextChar +} + +func (i *Input) NextChar() rune { + return i.nextChar +} diff --git a/rpn/rpn.go b/rpn/rpn.go new file mode 100644 index 0000000..558c8d5 --- /dev/null +++ b/rpn/rpn.go @@ -0,0 +1,108 @@ +package rpn + +import ( + "errors" + "math" +) + +var ErrExit = errors.New("exit") + +type RPN struct { + stack *Stack + lexer *Lexer +} + +func NewRPN() *RPN { + return &RPN{ + stack: &Stack{}, + lexer: &Lexer{}, + } +} + +func printHelp() { + println("RPN Calculator") + println("==============") + println("Commands:") + println(" +, -, *, /, %, ^, sqrt") + println(" pop, swap") + println(" help") + println(" exit (or Ctrl-D)") + println("Examples:") + println(" 1 2 + == 3") + println(" 2 3 4 + *") + println(" 2 3 4 + * 5 /") + println(" 9 sqrt") + println(" 2 3 swap -") + println(" exit") +} + +func (r *RPN) Eval(input string) error { + tokens, err := r.lexer.Parse(input) + if err != nil { + return err + } + + for _, token := range tokens { + switch token.Type { + case number: + r.stack.Push(token.Value) + case binaryOp: + a, b, err := r.stack.Pop2() + if err != nil { + return err + } + switch token.Operator { + case plus: + r.stack.Push(b + a) + case minus: + r.stack.Push(b - a) + case multiply: + r.stack.Push(b * a) + case divide: + if a == 0 { + r.stack.Push(b) + r.stack.Push(a) + return errors.New("can't divide by zero") + } + r.stack.Push(b / a) + case mod: + r.stack.Push(float64(int64(b) % int64(a))) + case power: + r.stack.Push(math.Pow(b, a)) + } + case unaryOp: + a, err := r.stack.Pop() + if err != nil { + return err + } + switch token.Operator { + case sqrt: + r.stack.Push(math.Sqrt(a)) + } + case pop: + _, err := r.stack.Pop() + if err != nil { + return err + } + case swap: + err := r.stack.Swap() + if err != nil { + return err + } + case clr: + r.stack.Clear() + case help: + printHelp() + case exit: + return ErrExit + default: + return errors.New("Unknown operation") + } + } + + return nil +} + +func (r *RPN) PrintStack() { + r.stack.Print() +} diff --git a/rpn/stack.go b/rpn/stack.go new file mode 100644 index 0000000..3183349 --- /dev/null +++ b/rpn/stack.go @@ -0,0 +1,71 @@ +package rpn + +import ( + "errors" + "fmt" + "math" +) + +var ( + ErrStackEmpty = errors.New("stack is empty") + ErrNotEnoughValues = errors.New("not enough values on stack") +) + +type Stack struct { + values []float64 +} + +func (s *Stack) Push(value float64) { + s.values = append(s.values, value) +} + +func (s *Stack) Pop() (float64, error) { + count := s.Len() + if count == 0 { + return 0, ErrStackEmpty + } + value := s.values[count-1] + s.values = s.values[:count-1] + return value, nil +} + +func (s *Stack) Pop2() (float64, float64, error) { + count := s.Len() + if count < 2 { + return 0, 0, ErrNotEnoughValues + } + a := s.values[count-1] + b := s.values[count-2] + s.values = s.values[:count-2] + return a, b, nil +} + +func (s *Stack) Len() int { + return len(s.values) +} + +func (s *Stack) Swap() error { + count := s.Len() + if count < 2 { + return ErrNotEnoughValues + } + a := s.values[count-1] + b := s.values[count-2] + s.values[count-1] = b + s.values[count-2] = a + return nil +} + +func (s *Stack) Clear() { + s.values = []float64{} +} + +func (s *Stack) Print() { + for _, value := range s.values { + if value == math.Trunc(value) { + fmt.Printf("%d\n", int64(value)) + } else { + fmt.Printf("%f\n", value) + } + } +} diff --git a/rpn/token.go b/rpn/token.go new file mode 100644 index 0000000..1d0623c --- /dev/null +++ b/rpn/token.go @@ -0,0 +1,35 @@ +package rpn + +type TokenType int + +const ( + number TokenType = iota + binaryOp + unaryOp + dec + bin + hex + pop + swap + clr + help + exit +) + +type Operator int + +const ( + plus Operator = iota + minus + multiply + divide + mod + power + sqrt +) + +type Token struct { + Type TokenType + Operator Operator + Value float64 +}