move develop to codeberg

This commit is contained in:
2025-11-02 10:53:03 +01:00
parent 70b455a19f
commit 328d08cf95
45 changed files with 1 additions and 5314 deletions

View File

@@ -1,96 +0,0 @@
prSections:
- title: Responsible PRs
filters: repo:tlinden/rpnc is:open NOT dependabot
layout:
repoName:
hidden: true
- title: Responsible Dependabot PRs
filters: repo:tlinden/rpnc is:open dependabot
layout:
repoName:
hidden: true
issuesSections:
- title: Responsible Issues
filters: is:open repo:tlinden/rpnc -author:@me
layout:
repoName:
hidden: true
- title: Note-to-Self Issues
filters: is:open repo:tlinden/rpnc author:@me
layout:
creator:
hidden: true
repoName:
hidden: true
defaults:
preview:
open: false
width: 100
keybindings:
universal:
- key: "shift+down"
builtin: pageDown
- key: "shift+up"
builtin: pageUp
prs:
- key: g
name: gitu
command: >
cd {{.RepoPath}} && /home/scip/bin/gitu
- key: M
name: squash-merge
command: gh pr merge --rebase --squash --admin --repo {{.RepoName}} {{.PrNumber}}
- key: i
name: show ci checks
command: gh pr checks --repo {{.RepoName}} {{.PrNumber}} | glow -p
- key: e
name: edit pr
command: ~/.config/gh-dash/edit-gh-pr {{.RepoName}} {{.PrNumber}}
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
issues:
- key: v
name: view
command: gh issue view --repo {{.RepoName}} {{.IssueNumber}} | glow -p
- key: l
name: add label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --add-label $(gum choose bug enhancement question dependencies wontfix)
- key: L
name: remove label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --remove-label $(gum choose bug enhancement question dependencies wontfix)
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
theme:
ui:
sectionsShowCount: true
table:
compact: false
showSeparator: true
colors:
text:
primary: "#E2E1ED"
secondary: "#6770cb"
inverted: "#242347"
faint: "#b0793b"
warning: "#E0AF68"
success: "#3DF294"
background:
selected: "#1B1B33"
border:
primary: "#383B5B"
secondary: "#39386B"
faint: "#8d3e0b"
repoPaths:
:owner/:repo: ~/dev/:repo
pager:
diff: delta

View File

@@ -1,69 +0,0 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
- goos: linux
formats: [tar.gz,binary]
files:
- src: "*.md"
strip_parent: true
- src: "docs/*"
strip_parent: true
- src: Makefile.dist
dst: Makefile
wrap_in_directory: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Improved
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
order: 0
- title: Fixed
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
order: 1
- title: Changed
order: 999
release:
header: "# Release Notes"
footer: >-
---
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/rpnc/compare/{{ .PreviousTag }}...{{ .Tag }})

View File

@@ -1,27 +0,0 @@
matrix:
platform:
- linux/amd64
goversion:
- 1.24
labels:
platform: ${platform}
steps:
build:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go build
- go test
linter:
when:
event: [push]
image: golang:${goversion}
commands:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
- golangci-lint --version
- golangci-lint run ./...

View File

@@ -1,15 +0,0 @@
# build release
labels:
platform: linux/amd64
steps:
goreleaser:
image: goreleaser/goreleaser
when:
event: [tag]
environment:
GITEA_TOKEN:
from_secret: DEPLOY_TOKEN
commands:
- goreleaser release --clean --verbose

View File

@@ -1,97 +0,0 @@
# Copyright © 2023 Thomas von Dein
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# no need to modify anything below
tool = rpn
VERSION = $(shell grep VERSION main.go | head -1 | cut -d '"' -f2)
archs = darwin freebsd linux windows
PREFIX = /usr/local
UID = root
GID = 0
HAVE_POD := $(shell pod2text -h 2>/dev/null)
all: $(tool).1 cmd/$(tool).go buildlocal
%.1: %.pod
ifdef HAVE_POD
pod2man -c "User Commands" -r 1 -s 1 $*.pod > $*.1
endif
cmd/%.go: %.pod
ifdef HAVE_POD
echo "package main" > cmd/$*.go
echo >> cmd/$*.go
echo "var manpage = \`" >> cmd/$*.go
pod2text cmd/$*.pod >> cmd/$*.go
echo "\`" >> cmd/$*.go
endif
buildlocal:
CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o $(tool)
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) coverage.out testdata
test: clean
go test ./... $(ARGS)
testfuzzy: clean
go test -fuzz ./... $(ARGS)
testlint: test lint
lint:
golangci-lint run
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,godox,dupword,forcetypeassert,goerr113,gomnd
singletest:
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) $(ARGS)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
goupdate:
go get -t -u=patch ./...
buildall:
./mkrel.sh $(tool) $(VERSION)
release:
gh release create v$(VERSION) --generate-notes
show-versions: buildlocal
@echo "### rpn version:"
@./rpn -v
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod

View File

@@ -1,20 +0,0 @@
# -*-make-*-
.PHONY: install all
tool = rpn
PREFIX = /usr/local
UID = root
GID = 0
all:
@echo "Type 'sudo make install' to install the tool."
@echo "To change prefix, type 'sudo make install PREFIX=/opt'"
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
install -o $(UID) -g $(GID) -m 444 *.md $(PREFIX)/share/doc/

View File

@@ -1,4 +1,4 @@
[![status-badge](https://ci.codeberg.org/api/badges/15511/status.svg?branch=main)](https://ci.codeberg.org/repos/15511)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://codeberg.org/scip/rpnc/raw/branch/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/codeberg.org/scip/rpnc)](https://goreportcard.com/report/codeberg.org/scip/rpnc)

View File

@@ -1,264 +0,0 @@
## Reverse Polish Notation Calculator for the commandline
This is a small commandline calculator which takes its input in
[reverse polish notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation)
form.
It has an unlimited stack, supports various stack manipulation
commands, can be used interactively or via a pipe and has a collector
mode. It doesn't have any other dependencies than Perl.
## Usage
Calculate the summary resistance of parallel resistors with 220, 330
and 440 Ohm using the following formula:
1 / (1/R1 + 1/R2 + 1/R3)
Here's the sample session:
0 % 1
stack 1: 1
1 % 1
stack 2: 1
stack 1: 1
2 % 220
stack 3: 1
stack 2: 1
stack 1: 220
3 % /
stack 2: 1
stack 1: 0.00454545454545455
=> 0.00454545454545455
2 % 1
stack 3: 1
stack 2: 0.00454545454545455
stack 1: 1
3 % 330
stack 4: 1
stack 3: 0.00454545454545455
stack 2: 1
stack 1: 330
4 % /
stack 3: 1
stack 2: 0.00454545454545455
stack 1: 0.00303030303030303
=> 0.00303030303030303
3 % 1
stack 4: 1
stack 3: 0.00454545454545455
stack 2: 0.00303030303030303
stack 1: 1
4 % 440
stack 5: 1
stack 4: 0.00454545454545455
stack 3: 0.00303030303030303
stack 2: 1
stack 1: 440
5 % /
stack 4: 1
stack 3: 0.00454545454545455
stack 2: 0.00303030303030303
stack 1: 0.00227272727272727
=> 0.00227272727272727
4 % +
stack 3: 1
stack 2: 0.00454545454545455
stack 1: 0.0053030303030303
=> 0.0053030303030303
3 % +
stack 2: 1
stack 1: 0.00984848484848485
=> 0.00984848484848485
2 % /
stack 1: 101.538461538462
=> 101.538461538462
The *%* character denotes the interactive prompt. What we basically entered was:
1 1 220 / 1 330 / 1 440 / + + /
Which translates to:
1 ((1 / 220) + (1 / 330) + (1 / 440))
So, you're entering the numbers and operators as you would do on
paper. To learn more, refer to the Wikipedia page linked above.
## Collector mode
Beside traditional RPN you can also enter a special mode, called
*collector mode* by entering the <kbd>(</kbd> command. The collector
mode has its own stack (a sub stack) which is independed of the
primary stack. Inside this mode you can use all operators, however
they work on *ALL* items on the sub stack.
So, let's compare. If you had in normal RPN mode the following stack:
3
5
6
and then entering the <kbd>+</kbd> operator, the calculator would pop
5 and 6 from the stack, add them and push the result 11 back to the
stack.
However, if you are in collector mode with this stack, then all the
items would be added, the sub stack would be cleared and the result 14
would be added to the primary stack.
You will leave the collector mode after an operator has been
executed. But you can also just leave the collector mode with the
command <kbd>)</kbd> leaving the sub stack intact. That is, upon
re-entering collector mode at a later time, you'll find the unaltered
sub stack of before.
## Undo
Every operation which modifies the stack can be reversed by entering
the <kbd>u</kbd> command. There's only one level of undo and no redo.
## Functions
You can define functions anytime directly on the cli or in a file called
`~/.rpnc`. A function has a name (which must not collide with existing
functions and commands) and a body of commands.
Example:
f res2vcc 1.22 R1 R2 + R2 / 1 + *
Which calculates:
(((R1 + R2) / R2) + 1) * 1.22 = ??
To use it later, just enter the variables into the stack followed by the
function name:
470
220
res2vcc
=> 2.79
You can also put the function definition in the config file
`~/.rpnc`. Empty lines and lines beginning with `#` will be ignored.
Another way to define a function is to use perl code directly. The
perl code must be a closure string and surrounded by braces. You can
access the stack via `@_`. Here's an example:
f pr { return "1.0 / (" . join(' + ', map { "1.0 / $_"} @_) . ")" }
This function calculates the parallel resistance of a number of
resistors. It adds up all values from the stack. Usage:
22
47
330
pr
=> 41.14
## Using STDIN via a PIPE
If the commandline includes any operator, commands will be read from
STDIN, the result will be printed to STDOUT wihout any decoration and
the program will exit. Commands can be separated by whitespace or
newline.
Examples:
echo "2 2" | rpnc +
(echo 2; echo 2) | rpnc +
Both commands will print 4 to STDOUT.
## Complete list of all supported commands:
### Stack Management
* <kbd>s</kbd> show the stack
* <kbd>ss</kbd> show the whole stack
* <kbd>sc</kbd> clear stack
* <kbd>scx</kbd> clear last stack element
* <kbd>sr</kbd> reverse the stack
* <kbd>srt</kbd> rotate the stack
## Configuration
* <kbd>td</kbd> toggle debugging (-d)
* <kbd>ts</kbd> toggle display of the stack (-n)
## Supported mathematical operators:
* <kbd>+</kbd> add
* <kbd>-</kbd> substract
* <kbd>/</kbd> divide
* <kbd>*</kbd> multiply
* <kbd>^</kbd> expotentiate
* <kbd>%</kbd> percent
* <kbd>%+</kbd> add percent
* <kbd>%-</kbd> substract percent
* <kbd>%d</kbd> percentual difference
* <kbd>&</kbd> bitwise AND
* <kbd>|</kbd> bitwise OR
* <kbd>x</kbd> bitwise XOR
* <kbd>m</kbd> median
* <kbd>a</kbd> average
* <kbd>v</kbd> pull root (2nd if stack==1)
* <kbd>(</kbd> enter collect mode
* <kbd>)</kbd> leave collect mode
## Register Commands
* <kbd>r</kbd> put element into register
* <kbd>rc</kbd> clear register
* <kbd>rcx</kbd> clear last register element
## Various Commands
* <kbd>u</kbd> undo last operation
* <kbd>q</kbd> finish (<kbd>C-d</kbd> works as well)
* <kbd>h</kbd> show history of past operations
* <kbd>?</kbd> print help
## Converters
* <kbd>tl</kbd> gallons to liters
* <kbd>tk</kbd> miles to kilometers
* <kbd>tm</kbd> yards to meters
* <kbd>tc</kbd> inches to centimeters
* <kbd>tkb</kbd> bytes to kilobytes
* <kbd>tmb</kbd> bytes to megabytes
* <kbd>tgb</kbd> bytes to gigabytes
* <kbd>ttb</kbd> bytes to terabytes
## Function Comands
* <kbd>f NAME CODE</kbd> define a functions (see ab above)
* <kbd>fs</kbd> show list of defined functions
## Copyleft
Copyleft (L) 2019 - Thomas von Dein.
Licensed under the terms of the GPL 3.0.

View File

@@ -1,679 +0,0 @@
#!/usr/bin/perl
use Term::ReadLine;
use Data::Dumper;
use Getopt::Long;
use Data::Dumper;
use strict;
use warnings;
my (@stack, @substack, @backup, @subbackup, @hist, @register);
my $term = Term::ReadLine->new('rpn calc');
my $debug = 0;
my $showstack = 1;
my $mgt = 0;
my $tty = 1;
my $VERSION = '1.10';
my $sub = 0;
my $maxstack = 10;
my $maxreg = 5;
my $silent = 1;
my $op;
# management commands, always lower case letters or words
my %commands = (
# stack commands
s => sub { $mgt = 1; dumpstack(); $mgt = 0; },
sa => sub { $mgt = 1; dumpstack(1); $mgt = 0;},
sc => sub { clearstack(); },
scx => sub { clearstack(1); dumpstack(); },
sr => sub { reversestack(); },
srt => sub { rotatestack(); },
# collector
'(' => sub { $sub = 1 },
')' => sub { stack2sub(); },
# register stuff
r => sub { last_to_reg(); dumpstack(); },
rcx => sub { clearreg(1); dumpstack(); },
rc => sub { clearreg(); },
# main
'?' => sub { help(); },
u => sub { undo(); dumpstack(); },
h => sub { showhist(); },
q => sub { exit; },
# toggles
td => sub { $debug ^= 1; },
ts => sub { $showstack ^= 1; },
# functions
fs => sub { showfuncs(); },
);
# executed 1:1, or aliased
my %alias = qw(^ ** x ^ < << > >> + + - - / / * * & & | |);
# holds user functions
my %custom;
# hand coded functions
my %func = (
'%' => sub {
# X % of Y
my ($a, $b) = getlast(2);
if (defined $b) {
return "($a / 100) * $b";
}
},
'%d' => sub {
# percentual difference
my ($a, $b) = getlast(2);
if (defined $b) {
return "(($a - $b) / $b) * 100"
}
},
'%+' => sub {
# Y + (X $ of Y)
my ($a, $b) = getlast(2);
if (defined $b) {
return "$a + (($a / 100) * $b)";
}
},
'%-' => sub {
# Y - (X $ of Y)
my ($a, $b) = getlast(2);
if (defined $b) {
return "$a - (($a / 100) * $b)";
}
},
'v' => sub {
# square root
my ($a) = getlast(1);
if (defined $a) {
return "$a ** (1 / 2)";
}
},
'm' => sub {
# median
my @values = getlast(2); # we need 2 or all in sub mode
if (scalar @values >= 2) {
my $c = $#values;
if (scalar @values % 2 == 0) {
# even
return "((sort qw(@values))[$c / 2] + (sort qw(@values))[($c / 2) + 1]) / 2";
}
else {
# uneven
return "(sort qw(@values))[$c / 2]";
}
}
else {
print "median only possible with 2 or more values\n";
undo();
return 0;
}
},
'a' => sub {
# average
my @values = getlast(2); # we need 2 or all in sub mode
if (scalar @values > 1) {
return "(" . join(' + ', @values) . ") / " . scalar @values;
}
else {
print "average only possible with 2 or more values\n";
undo();
return 0;
}
},
# converters:
# gallons to liters
'tl' => sub { return convert("* 3.785") },
# yards to meters
'tm' => sub { return convert("* 91.44") },
# miles to kilometers
'tk' => sub { return convert("* 1.609") },
# inches to cm
'tc' => sub { return convert("* 2.54") },
# to 'bytes
'tkb' => sub { return convert("/ 1000") },
'tmb' => sub { return convert("/ 1000 / 1000") },
'tgb' => sub { return convert("/ 1000 / 1000 / 1000") },
'ttb' => sub { return convert("/ 1000 / 1000 / 1000 / 1000") },
);
# math constants, always upper case letters, usable via eval{}
use constant PI => 3.141592653589793;
use constant V2 => 1.414213562373095;
use constant V3 => 1.732050807568877;
# handle command line
my ($o_h, $o_v, $o_s);
Getopt::Long::Configure( qw(no_ignore_case));
if (! GetOptions (
"version|v" => \$o_v,
"help|h" => \$o_h,
"debug|d" => \$debug,
"nostack|n" => \$o_s
) ) {
help();
exit;
}
if ($o_v) {
print "$0 version $VERSION\n";
exit;
}
if ($o_h) {
help();
exit;
}
if ($o_s) {
$showstack = 0;
}
# load config, if any
if (-s "$ENV{HOME}/.rpnc") {
if (open RC, "< $ENV{HOME}/.rpnc") {
while (<RC>) {
chomp();
next if (/^\s*#/ || /^\s*$/);
looptokenize($_);
}
close RC;
$silent = 0;
}
}
# run in commandline mode?
$op = shift;
if ($op) {
$tty = 0;
while (<STDIN>) {
chomp;
push @stack, split /\s\s*/;
}
print calc($op);
exit;
}
# else: run interactively
# main
my $OUT = $term->OUT || \*STDOUT;
while ( defined ($_ = $term->readline(prompt())) ) {
looptokenize($_);
}
1;
# converter helper
sub convert {
my $code = shift;
my ($a) = getlast(1);
if (defined $a) {
return "$a $code";
}
}
sub looptokenize {
# disassemble user input into tokens
my $tokens = shift;
if ($tokens =~ /^f\s/) {
# function definition
defun($tokens);
}
else {
foreach my $tok (split /\s+/, $tokens) {
if ($tok =~ /^-?[A-Z\.\d]+?$/) {
# number or register fetch
if ($tok =~ /^R(\d+?)/) {
# fetch number from register $1 and put it to stack
my $r = getreg($1);
if ($r) {
pushstack($r);
}
else {
print "invalid register index!\n";
next;
}
}
else {
# put number to stsack
pushstack($tok);
}
dumpstack();
}
else {
# operator or command, execute
if (exists $commands{$tok}) {
cmd($tok);
}
else {
print calc($tok);
}
}
}
}
}
sub cmd {
my $c = shift;
if (exists $commands{$c}) {
my $sub = $commands{$c};
&$sub;
}
else {
print "unknown command '$c'!\n";
}
}
sub showhist {
foreach my $entry (@hist) {
printf "History: %10s = %s\n", $entry->[0], $entry->[1];
}
}
sub clearstack {
my $one = shift;
backup();
if ($sub) {
if ($one) {
pop @substack;
}
else {
@substack = ();
}
}
else {
if ($one) {
pop @stack;
}
else {
@stack = ();
}
}
}
sub reversestack {
backup();
if ($sub) {
@substack = reverse @substack;
}
else {
@stack = reverse @stack;
}
dumpstack();
}
sub rotatestack {
backup();
if ($sub) {
my $f = shift @substack;
@substack = (@substack, $f);
}
else {
my $f = shift @stack;
@stack = (@stack, $f);
}
dumpstack();
}
sub pushstack {
my $num = shift;
if ($num) {
if ($num =~ /^\./) {
$num = '0' . $num;
}
if ($sub) {
push @substack, $num;
}
else {
push @stack, $num;
}
}
}
sub dumpstack {
if (! $showstack && !$mgt) {
return;
}
my $max = shift;
my $x = ' ';
my $prefix = 'stack';
my @all;
if ($sub) {
@all = @substack;
$prefix = 'collectorstack';
}
else {
@all = @stack;
}
my $abs = scalar @all;
if (! $max && $abs > $maxstack) {
my $min = $max - ($max * 2);
@all = @all[$min .. -1];
printf "%s [..]\n", $prefix;
}
if (@register) {
my $p = 1;
foreach my $n (@register) {
printf "register R%d: %s\n", $p++, $n;
}
}
print "\n";
my $p = scalar @all;
foreach my $n (@all) {
$x = 'X' if($p == 1);
printf "%s %s %4d: %s\n", $prefix, $x, $p--, $n;
}
print "\n";
}
sub undo {
if ($sub) {
@substack = @subbackup;
}
else {
@stack = @backup;
}
}
sub backup {
if ($sub) {
@subbackup = @substack;
}
else {
@backup = @stack;
}
}
sub getlast {
# return and remove last 1, 2 or all elements of current stack
my $request = shift;
my @all = ();
backup();
if ($sub) {
# ignore request count
@all = @substack;
@substack = ();
}
else {
if (@stack) {
if (scalar @stack == 1) {
if ($request > 1) {
print "At least $request variables must be on the stack!\n";
}
else {
@all = pop @stack;
}
}
elsif (scalar @stack >= 2) {
@all = splice(@stack, -1 * $request, $request);
}
}
else {
print "Please enter one or more numbers to operate on!\n";
}
}
return @all;
}
sub getreg {
# fetch $n'th element from register
my $n = shift;
if ($n <= scalar @register) {
return $register[$n-1];
}
else {
return 0;
}
}
sub last_to_reg {
# put last stack element to register
my $n;
if ($sub) {
if (@substack) {
$n = $substack[-1];
}
}
else {
if (@stack) {
$n = $stack[-1];
}
}
if ($n) {
if (scalar @register == $maxreg) {
shift @register;
}
push @register, $n;
}
}
sub clearreg {
my $one = shift;
if ($one) {
pop @register;
}
else {
@register = ();
}
}
sub stack2sub {
if (! $sub && scalar @substack == 0 && scalar @stack > 1) {
# not in collector mode, empty substack, move stack to substack, enter collect
backup();
@substack = @stack;
@stack = ();
$sub = 1;
}
else {
# leave collector mode
$sub = 0;
}
}
sub prompt {
my $count;
my $prompt;
if ($sub) {
$count = scalar @substack;
$prompt = '%--(';
}
else {
$count = scalar @stack;
$prompt = '%';
}
return sprintf "%3d %s ", $count, $prompt;
}
sub calc {
my $op = shift;
my $res;
my $code;
if (exists $alias{$op}) {
my @last = getlast(2);
$op = $alias{$op};
$code = join(" $op ", @last);
}
elsif (exists $func{$op}) {
my $sub = $func{$op};
$code = &$sub();
return unless $code;
}
else {
print "syntax error or unknown command ($op)!\n";
undo();
return;
}
# execute
eval "\$res = $code";
if ($@) {
# error, reset stack
print "Syntax error: $@, resetting stack\n";
undo();
}
else {
push @stack, $res;
$sub = 0;
if ($debug) {
print "DEBUG: $code = $res\n";
}
if ($tty) {
dumpstack();
push @hist, [$res, $code];
return "=> $res\n\n";
}
else {
return "$res\n";
}
}
}
sub defun {
# define a function, use N1 .. NN as function arguments
my $code = shift;
my ($op, $name, @tokens) = split /\s\s*/, $code;
if ($name !~ /^[a-zA-Z0-9_]+$/) {
print "invalid function name (a-z0-9_)!\n";
return;
}
if (! exists $custom{$name}) {
# no need to check twice and overwriting of custom function must be legal
if (grep {$name eq $_} keys %commands) {
print "reserved function name (command)!\n";
return;
}
if (grep {$name eq $_} keys %func) {
print "reserved function name (function)!\n";
return;
}
}
$custom{$name} = "@tokens";
if ($custom{$name} =~ /^\{.*\}$/) {
# perl code
$func{$name} = sub { return eval "@tokens" };
}
else {
# rpnc code
$func{$name} = sub {
my $max = scalar @_;
my @args = reverse(@_);
# replace N1..NN with actual stack items
my @body;
foreach my $item (@tokens) {
if ($item =~ /^([A-Z])(\d+)$/) {
my $letter = $1;
my $i = $2;
if ($i <= $max) {
push @body, $args[$i-1];
}
else {
print "undefined variable ${letter}${i}!\n";
push @body, 0;
}
}
else {
push @body, $item;
}
}
# execute @body
looptokenize("@body");
};
}
print "function $name() defined.\n" unless $silent;
}
sub showfuncs {
foreach my $f (sort keys %custom) {
print "Function $f():\n $custom{$f}\n\n";
}
}
sub help {
print qq~
Reverse Polish Notation Calculator, version $VERSION.
Copyleft (L) 2019-2020 - Thomas von Dein.
Licensed under the terms of the GPL 3.0.
Commandline: rpn [-d] [<operator>]
If <operator> is provided, read numbers from STDIN,
otherwise runs interactively.
Configure: Available math operators:
td toggle debugging (-d) ( enter collect mode
ts toggle display of stack (-n) ) leave collect || stack => collect
+ add
Stack Management: - substract
s show the stack / divide
sa show the whole stack * multiply
scx clear X (last stack element) ^ expotentiate
sc clear stack % percent (%+ add %- substract)
sr reverse the stack %d percentual difference
srt rotate the stack & bitwise AND
| bitwise OR
Register Management: x bitwise XOR
r put X to register < > bitwise shift left or right
R1-9 push value of register to stack v square root
rcx clear X (last register element) m median
rc clear register a average
Converters:
tl gallons => liters tkb bytes => kb
tk miles => kilometers tmb bytes => mb
tm yards => meters tgb bytes => gb
tc inches => centimeters ttb bytes => tb
Various Commands: Functions:
u undo last operation f <name> op op... (use N1..NN for stack)
h show history of past operations fs show list of defined functions
q finish (C-d works as well) Using register: enter R + index, e.g. R1
? print help Constants: PI V2 V3
~;
}

View File

@@ -1,624 +0,0 @@
/*
Copyright © 2023-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"errors"
"fmt"
"math"
"regexp"
"sort"
"strconv"
"strings"
"github.com/chzyer/readline"
)
type Calc struct {
debug bool
batch bool
stdin bool
showstack bool
intermediate bool
notdone bool // set to true as long as there are items left in the eval loop
precision int
stack *Stack
history []string
completer readline.AutoCompleter
interpreter *Interpreter
Space *regexp.Regexp
Comment *regexp.Regexp
Register *regexp.Regexp
Constants []string
LuaFunctions []string
Funcalls Funcalls
BatchFuncalls Funcalls
// different kinds of commands, displays nicer in help output
StackCommands Commands
SettingsCommands Commands
ShowCommands Commands
Commands Commands
Vars map[string]float64
}
// help for lua functions will be added dynamically
const Help string = `
Operators:
basic operators: + - x * / ^ (* is an alias of x)
Bitwise operators: and or xor < (left shift) > (right shift)
Percent functions:
% percent
%- subtract percent
%+ add percent
Math functions (see https://pkg.go.dev/math):
mod sqrt abs acos acosh asin asinh atan atan2 atanh cbrt ceil cos cosh
erf erfc erfcinv erfinv exp exp2 expm1 floor gamma ilogb j0 j1 log
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot
Converter functions:
cm-to-inch yards-to-meters bytes-to-kilobytes
inch-to-cm meters-to-yards bytes-to-megabytes
gallons-to-liters miles-to-kilometers bytes-to-gigabytes
liters-to-gallons kilometers-to-miles bytes-to-terabytes
Batch functions:
sum sum of all values (alias: +)
max max of all values
min min of all values
mean mean of all values (alias: avg)
median median of all values
Register variables:
>NAME Put last stack element into variable NAME
<NAME Retrieve variable NAME and put onto stack`
// commands, constants and operators, defined here to feed completion
// and our mode switch in Eval() dynamically
const (
Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E`
Precision int = 2
ShowStackLen int = 5
)
// That way we can add custom functions to completion
func GetCompleteCustomFunctions() func(string) []string {
return func(line string) []string {
completions := []string{}
for luafunc := range LuaFuncs {
completions = append(completions, luafunc)
}
completions = append(completions, strings.Split(Constants, " ")...)
return completions
}
}
func (c *Calc) GetCompleteCustomFuncalls() func(string) []string {
return func(line string) []string {
completions := []string{}
for function := range c.Funcalls {
completions = append(completions, function)
}
for function := range c.BatchFuncalls {
completions = append(completions, function)
}
for command := range c.SettingsCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.ShowCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.StackCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.Commands {
if len(command) > 1 {
completions = append(completions, command)
}
}
return completions
}
}
func NewCalc() *Calc {
calc := Calc{stack: NewStack(), debug: false, precision: Precision}
calc.Funcalls = DefineFunctions()
calc.BatchFuncalls = DefineBatchFunctions()
calc.Vars = map[string]float64{}
calc.completer = readline.NewPrefixCompleter(
// custom lua functions
readline.PcItemDynamic(GetCompleteCustomFunctions()),
readline.PcItemDynamic(calc.GetCompleteCustomFuncalls()),
)
calc.Space = regexp.MustCompile(`\s+`)
calc.Comment = regexp.MustCompile(`#.*`) // ignore everything after #
calc.Register = regexp.MustCompile(`^([<>])([A-Z][A-Z0-9]*)`)
// pre-calculate mode switching arrays
calc.Constants = strings.Split(Constants, " ")
calc.SetCommands()
return &calc
}
// setup the interpreter, called from main(), import lua functions
func (c *Calc) SetInt(interpreter *Interpreter) {
c.interpreter = interpreter
for name := range LuaFuncs {
c.LuaFunctions = append(c.LuaFunctions, name)
}
}
func (c *Calc) ToggleDebug() {
c.debug = !c.debug
c.stack.ToggleDebug()
fmt.Printf("debugging set to %t\n", c.debug)
}
func (c *Calc) ToggleBatch() {
c.batch = !c.batch
fmt.Printf("batchmode set to %t\n", c.batch)
}
func (c *Calc) ToggleStdin() {
c.stdin = !c.stdin
}
func (c *Calc) ToggleShow() {
c.showstack = !c.showstack
}
func (c *Calc) Prompt() string {
prompt := "\033[31m»\033[0m "
batch := ""
if c.batch {
batch = "->batch"
}
debug := ""
revision := ""
if c.debug {
debug = "->debug"
revision = fmt.Sprintf("/rev%d", c.stack.rev)
}
return fmt.Sprintf("rpn%s%s [%d%s]%s", batch, debug, c.stack.Len(), revision, prompt)
}
// the actual work horse, evaluate a line of calc command[s]
func (c *Calc) Eval(line string) error {
// remove surrounding whitespace and comments, if any
line = strings.TrimSpace(c.Comment.ReplaceAllString(line, ""))
if line == "" {
return nil
}
items := c.Space.Split(line, -1)
for pos, item := range items {
if pos+1 < len(items) {
c.notdone = true
} else {
c.notdone = false
}
if err := c.EvalItem(item); err != nil {
return err
}
}
if c.showstack && !c.stdin {
dots := ""
if c.stack.Len() > ShowStackLen {
dots = "... "
}
last := c.stack.Last(ShowStackLen)
fmt.Printf("stack: %s%s\n", dots, list2str(last))
}
return nil
}
func (c *Calc) EvalItem(item string) error {
num, err := strconv.ParseFloat(item, 64)
if err == nil {
c.stack.Backup()
c.stack.Push(num)
return nil
}
// try time
var hour, min int
_, err = fmt.Sscanf(item, "%d:%d", &hour, &min)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(hour) + float64(min)/60)
return nil
}
// try hex
var i int
_, err = fmt.Sscanf(item, "0x%x", &i)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(i))
return nil
}
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
return nil
}
if exists(c.Funcalls, item) {
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
}
c.Result()
return nil
}
if exists(c.BatchFuncalls, item) {
if !c.batch {
return Error("only supported in batch mode")
}
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
}
c.Result()
return nil
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.EvalLuaFunction(item)
return nil
}
regmatches := c.Register.FindStringSubmatch(item)
if len(regmatches) == 3 {
switch regmatches[1] {
case ">":
c.PutVar(regmatches[2])
case "<":
c.GetVar(regmatches[2])
}
return nil
}
// internal commands
// FIXME: propagate errors
if exists(c.Commands, item) {
c.Commands[item].Func(c)
return nil
}
if exists(c.ShowCommands, item) {
c.ShowCommands[item].Func(c)
return nil
}
if exists(c.StackCommands, item) {
c.StackCommands[item].Func(c)
return nil
}
if exists(c.SettingsCommands, item) {
c.SettingsCommands[item].Func(c)
return nil
}
switch item {
case "?", "help":
c.PrintHelp()
default:
return Error("unknown command or operator")
}
return nil
}
// Execute a math function, check if it is defined just in case
func (c *Calc) DoFuncall(funcname string) error {
var function *Funcall
if c.batch {
function = c.BatchFuncalls[funcname]
} else {
function = c.Funcalls[funcname]
}
if function == nil {
return Error("function not defined but in completion list")
}
var args Numbers
batch := false
if function.Expectargs == -1 {
// batch mode, but always < stack len, so check first
args = c.stack.All()
batch = true
} else {
// this is way better behavior than just using 0 in place of
// non-existing stack items
if c.stack.Len() < function.Expectargs {
return errors.New("stack doesn't provide enough arguments")
}
args = c.stack.Last(function.Expectargs)
}
c.Debug(fmt.Sprintf("calling %s with args: %v", funcname, args))
// the actual lambda call, so to say. We provide a slice of
// the requested size, fetched from the stack (but not popped
// yet!)
funcresult := function.Func(args)
if funcresult.Err != nil {
// leave the stack untouched in case of any error
return funcresult.Err
}
// don't forget to backup!
c.stack.Backup()
// "pop"
if batch {
// get rid of stack
c.stack.Clear()
} else {
// remove operands
c.stack.Shift(function.Expectargs)
}
// save result
c.stack.Push(funcresult.Res)
// thanks a lot
c.SetHistory(funcname, args, funcresult.Res)
return nil
}
// we need to add a history entry for each operation
func (c *Calc) SetHistory(op string, args Numbers, res float64) {
c.History("%s %s -> %f", list2str(args), op, res)
}
// just a textual representation of math operations, viewable with the
// history command
func (c *Calc) History(format string, args ...any) {
c.history = append(c.history, fmt.Sprintf(format, args...))
}
// print the result
func (c *Calc) Result() float64 {
// we only print the result if it's either a final result or
// (if it is intermediate) if -i has been given
if c.intermediate || !c.notdone {
// only needed in repl
if !c.stdin {
fmt.Print("= ")
}
result := c.stack.Last()[0]
truncated := math.Trunc(result)
precision := c.precision
if result == truncated {
precision = 0
}
format := fmt.Sprintf("%%.%df\n", precision)
fmt.Printf(format, result)
}
return c.stack.Last()[0]
}
func (c *Calc) Debug(msg string) {
if c.debug {
fmt.Printf("DEBUG(calc): %s\n", msg)
}
}
func (c *Calc) EvalLuaFunction(funcname string) {
// called from calc loop
var luaresult float64
var err error
switch c.interpreter.FuncNumArgs(funcname) {
case 0:
fallthrough
case 1:
luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last())
case 2:
luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last(2))
case -1:
luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.All())
default:
luaresult, err = 0, errors.New("invalid number of argument requested")
}
if err != nil {
fmt.Println(err)
return
}
c.stack.Backup()
dopush := true
switch c.interpreter.FuncNumArgs(funcname) {
case 0:
a := c.stack.Last()
if len(a) == 1 {
c.History("%s(%f) = %f", funcname, a, luaresult)
}
dopush = false
case 1:
a := c.stack.Pop()
c.History("%s(%f) = %f", funcname, a, luaresult)
case 2:
a := c.stack.Pop()
b := c.stack.Pop()
c.History("%s(%f,%f) = %f", funcname, a, b, luaresult)
case -1:
c.stack.Clear()
c.History("%s(*) = %f", funcname, luaresult)
}
if dopush {
c.stack.Push(luaresult)
}
c.Result()
}
func (c *Calc) PutVar(name string) {
last := c.stack.Last()
if len(last) == 1 {
c.Debug(fmt.Sprintf("register %.2f in %s", last[0], name))
c.Vars[name] = last[0]
} else {
fmt.Println("empty stack")
}
}
func (c *Calc) GetVar(name string) {
if exists(c.Vars, name) {
c.Debug(fmt.Sprintf("retrieve %.2f from %s", c.Vars[name], name))
c.stack.Backup()
c.stack.Push(c.Vars[name])
} else {
fmt.Println("variable doesn't exist")
}
}
func sortcommands(hash Commands) []string {
keys := make([]string, 0, len(hash))
for key := range hash {
if len(key) > 1 {
keys = append(keys, key)
}
}
sort.Strings(keys)
return keys
}
func (c *Calc) PrintHelp() {
output := "Available configuration commands:\n"
for _, name := range sortcommands(c.SettingsCommands) {
output += fmt.Sprintf("%-20s %s\n", name, c.SettingsCommands[name].Help)
}
output += "\nAvailable show commands:\n"
for _, name := range sortcommands(c.ShowCommands) {
output += fmt.Sprintf("%-20s %s\n", name, c.ShowCommands[name].Help)
}
output += "\nAvailable stack manipulation commands:\n"
for _, name := range sortcommands(c.StackCommands) {
output += fmt.Sprintf("%-20s %s\n", name, c.StackCommands[name].Help)
}
output += "\nOther commands:\n"
for _, name := range sortcommands(c.Commands) {
output += fmt.Sprintf("%-20s %s\n", name, c.Commands[name].Help)
}
output += "\n" + Help
// append lua functions, if any
if len(LuaFuncs) > 0 {
output += "\nLua functions:\n"
for name, function := range LuaFuncs {
output += fmt.Sprintf("%-20s %s\n", name, function.help)
}
}
Pager("rpn help overview", output)
}

View File

@@ -1,424 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"fmt"
"strconv"
"strings"
"testing"
lua "github.com/yuin/gopher-lua"
)
func TestCommentsAndWhitespace(t *testing.T) {
calc := NewCalc()
var tests = []struct {
name string
cmd []string
exp float64 // last element of the stack
}{
{
name: "whitespace prefix",
cmd: []string{" 5"},
exp: 5.0,
},
{
name: "whitespace postfix",
cmd: []string{"5 "},
exp: 5.0,
},
{
name: "whitespace both",
cmd: []string{" 5 "},
exp: 5.0,
},
{
name: "comment line w/ spaces",
cmd: []string{"5", " # 19"},
exp: 5.0,
},
{
name: "comment line w/o spaces",
cmd: []string{"5", `#19`},
exp: 5.0,
},
{
name: "inline comment w/ spaces",
cmd: []string{"5 # 19"},
exp: 5.0,
},
{
name: "inline comment w/o spaces",
cmd: []string{"5#19"},
exp: 5.0,
},
}
for _, test := range tests {
testname := fmt.Sprintf("%s .(expect %.2f)",
test.name, test.exp)
t.Run(testname, func(t *testing.T) {
for _, line := range test.cmd {
if err := calc.Eval(line); err != nil {
t.Error(err.Error())
}
}
got := calc.stack.Last()
if len(got) > 0 {
if got[0] != test.exp {
t.Errorf("parsing failed:\n+++ got: %f\n--- want: %f",
got, test.exp)
}
}
if calc.stack.Len() != 1 {
t.Errorf("invalid stack size:\n+++ got: %d\n--- want: 1",
calc.stack.Len())
}
})
calc.stack.Clear()
}
}
func TestCalc(t *testing.T) {
calc := NewCalc()
var tests = []struct {
name string
cmd string
exp float64
batch bool
}{
// ops
{
name: "plus",
cmd: `15 15 +`,
exp: 30,
},
{
name: "power",
cmd: `4 2 ^`,
exp: 16,
},
{
name: "minus",
cmd: `100 50 -`,
exp: 50,
},
{
name: "multi",
cmd: `4 4 x`,
exp: 16,
},
{
name: "divide",
cmd: `10 2 /`,
exp: 5,
},
{
name: "percent",
cmd: `400 20 %`,
exp: 80,
},
{
name: "percent-minus",
cmd: `400 20 %-`,
exp: 320,
},
{
name: "percent-plus",
cmd: `400 20 %+`,
exp: 480,
},
// math tests
{
name: "mod",
cmd: `9 2 mod`,
exp: 1,
},
{
name: "sqrt",
cmd: `16 sqrt`,
exp: 4,
},
{
name: "ceil",
cmd: `15.5 ceil`,
exp: 16,
},
{
name: "dim",
cmd: `6 4 dim`,
exp: 2,
},
// constants tests
{
name: "pitimes2",
cmd: `Pi 2 *`,
exp: 6.283185307179586,
},
{
name: "pi+sqrt2",
cmd: `Pi Sqrt2 +`,
exp: 4.555806215962888,
},
// batch tests
{
name: "batch-sum",
cmd: `2 2 2 2 sum`,
exp: 8,
batch: true,
},
{
name: "batch-median",
cmd: `1 2 3 4 5 median`,
exp: 3,
batch: true,
},
{
name: "batch-mean",
cmd: `2 2 8 2 2 mean`,
exp: 3.2,
batch: true,
},
{
name: "batch-min",
cmd: `1 2 3 4 5 min`,
exp: 1,
batch: true,
},
{
name: "batch-max",
cmd: `1 2 3 4 5 max`,
exp: 5,
batch: true,
},
// stack tests
{
name: "use-vars",
cmd: `10 >TEN clear 5 <TEN *`,
exp: 50,
},
{
name: "reverse",
cmd: `100 500 reverse -`,
exp: 400,
},
{
name: "swap",
cmd: `2 16 swap /`,
exp: 8,
},
{
name: "clear batch",
cmd: "1 1 1 1 1 clear 1 1 sum",
exp: 2,
batch: true,
},
{
name: "undo",
cmd: `4 4 + undo *`,
exp: 16,
},
// bit tests
{
name: "bit and",
cmd: `1 3 and`,
exp: 1,
},
{
name: "bit or",
cmd: `1 3 or`,
exp: 3,
},
{
name: "bit xor",
cmd: `1 3 xor`,
exp: 2,
},
// converters
{
name: "inch-to-cm",
cmd: `111 inch-to-cm`,
exp: 281.94,
},
{
name: "gallons-to-liters",
cmd: `111 gallons-to-liters`,
exp: 420.135,
},
{
name: "meters-to-yards",
cmd: `111 meters-to-yards`,
exp: 1.2139107611548556,
},
{
name: "miles-to-kilometers",
cmd: `111 miles-to-kilometers`,
exp: 178.599,
},
}
for _, test := range tests {
testname := fmt.Sprintf("cmd-%s-expect-%.2f",
test.name, test.exp)
t.Run(testname, func(t *testing.T) {
calc.batch = test.batch
if err := calc.Eval(test.cmd); err != nil {
t.Error(err.Error())
}
got := calc.Result()
calc.stack.Clear()
if got != test.exp {
t.Errorf("calc failed:\n+++ got: %f\n--- want: %f",
got, test.exp)
}
})
}
}
func TestCalcLua(t *testing.T) {
var tests = []struct {
function string
stack []float64
exp float64
}{
{
function: "lower",
stack: []float64{5, 6},
exp: 5.0,
},
{
function: "parallelresistance",
stack: []float64{100, 200, 300},
exp: 54.54545454545455,
},
}
calc := NewCalc()
LuaInterpreter = lua.NewState(lua.Options{SkipOpenLibs: true})
defer LuaInterpreter.Close()
luarunner := NewInterpreter("../example.lua", false)
luarunner.InitLua()
calc.SetInt(luarunner)
for _, test := range tests {
testname := fmt.Sprintf("lua-%s", test.function)
t.Run(testname, func(t *testing.T) {
calc.stack.Clear()
for _, item := range test.stack {
calc.stack.Push(item)
}
calc.EvalLuaFunction(test.function)
got := calc.stack.Last()
if calc.stack.Len() != 1 {
t.Errorf("invalid stack size:\n+++ got: %d\n--- want: 1",
calc.stack.Len())
}
if got[0] != test.exp {
t.Errorf("lua function %s failed:\n+++ got: %f\n--- want: %f",
test.function, got, test.exp)
}
})
}
}
func FuzzEval(f *testing.F) {
legal := []string{
"dump",
"showstack",
"help",
"Pi 31 *",
"SqrtE Pi /",
"55.5 yards-to-meters",
"2 4 +",
"7 8 batch sum",
"7 8 %-",
"7 8 clear",
"7 8 /",
"b",
"#444",
"<X",
"?",
"help",
}
for _, item := range legal {
f.Add(item)
}
calc := NewCalc()
var hexnum, hour, min int
f.Fuzz(func(t *testing.T, line string) {
t.Logf("Stack:\n%v\nLine: <%s>\n", calc.stack.All(), line)
switch line {
case "help", "?":
return
}
if err := calc.EvalItem(line); err == nil {
t.Logf("given: <%s>", line)
// not corpus and empty?
if !contains(legal, line) && len(line) > 0 {
item := strings.TrimSpace(calc.Comment.ReplaceAllString(line, ""))
_, hexerr := fmt.Sscanf(item, "0x%x", &hexnum)
_, timeerr := fmt.Sscanf(item, "%d:%d", &hour, &min)
// no comment?
if len(item) > 0 {
// no known command or function?
if _, err := strconv.ParseFloat(item, 64); err != nil {
if !contains(calc.Constants, item) &&
!exists(calc.Funcalls, item) &&
!exists(calc.BatchFuncalls, item) &&
!contains(calc.LuaFunctions, item) &&
!exists(calc.Commands, item) &&
!exists(calc.ShowCommands, item) &&
!exists(calc.SettingsCommands, item) &&
!exists(calc.StackCommands, item) &&
!calc.Register.MatchString(item) &&
item != "?" && item != "help" &&
hexerr != nil &&
timeerr != nil {
t.Errorf("Fuzzy input accepted: <%s>", line)
}
}
}
}
}
})
}

View File

@@ -1,361 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"bufio"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
)
type CommandFunction func(*Calc)
type Command struct {
Help string
Func CommandFunction
}
type Commands map[string]*Command
func NewCommand(help string, function CommandFunction) *Command {
return &Command{
Help: help,
Func: function,
}
}
func (c *Calc) SetSettingsCommands() Commands {
return Commands{
// Toggles
"debug": NewCommand(
"toggle debugging",
func(c *Calc) {
c.ToggleDebug()
},
),
"nodebug": NewCommand(
"disable debugging",
func(c *Calc) {
c.debug = false
c.stack.debug = false
},
),
"batch": NewCommand(
"toggle batch mode",
func(c *Calc) {
c.ToggleBatch()
},
),
"nobatch": NewCommand(
"disable batch mode",
func(c *Calc) {
c.batch = false
},
),
"showstack": NewCommand(
"toggle show last 5 items of the stack",
func(c *Calc) {
c.ToggleShow()
},
),
"noshowstack": NewCommand(
"disable display of the stack",
func(c *Calc) {
c.showstack = false
},
),
}
}
func (c *Calc) SetShowCommands() Commands {
return Commands{
// Display commands
"dump": NewCommand(
"display the stack contents",
func(c *Calc) {
c.stack.Dump()
},
),
"history": NewCommand(
"display calculation history",
func(c *Calc) {
for _, entry := range c.history {
fmt.Println(entry)
}
},
),
"vars": NewCommand(
"show list of variables",
func(c *Calc) {
if len(c.Vars) > 0 {
fmt.Printf("%-20s %s\n", "VARIABLE", "VALUE")
for k, v := range c.Vars {
fmt.Printf("%-20s -> %.2f\n", k, v)
}
} else {
fmt.Println("no vars registered")
}
},
),
"hex": NewCommand(
"show last stack item in hex form (converted to int)",
func(c *Calc) {
if c.stack.Len() > 0 {
fmt.Printf("0x%x\n", int(c.stack.Last()[0]))
}
},
),
}
}
func (c *Calc) SetStackCommands() Commands {
return Commands{
"clear": NewCommand(
"clear the whole stack",
func(c *Calc) {
c.stack.Backup()
c.stack.Clear()
},
),
"shift": NewCommand(
"remove the last element of the stack",
func(c *Calc) {
c.stack.Backup()
c.stack.Shift()
},
),
"reverse": NewCommand(
"reverse the stack elements",
func(c *Calc) {
c.stack.Backup()
c.stack.Reverse()
},
),
"swap": NewCommand(
"exchange the last two elements",
CommandSwap,
),
"undo": NewCommand(
"undo last operation",
func(c *Calc) {
c.stack.Restore()
},
),
"dup": NewCommand(
"duplicate last stack item",
CommandDup,
),
"edit": NewCommand(
"edit the stack interactively",
CommandEdit,
),
}
}
// define all management (that is: non calculation) commands
func (c *Calc) SetCommands() {
c.SettingsCommands = c.SetSettingsCommands()
c.ShowCommands = c.SetShowCommands()
c.StackCommands = c.SetStackCommands()
// general commands
c.Commands = Commands{
"exit": NewCommand(
"exit program",
func(c *Calc) {
os.Exit(0)
},
),
"manual": NewCommand(
"show manual",
func(c *Calc) {
man()
},
),
}
// aliases
c.Commands["quit"] = c.Commands["exit"]
c.SettingsCommands["d"] = c.SettingsCommands["debug"]
c.SettingsCommands["b"] = c.SettingsCommands["batch"]
c.SettingsCommands["s"] = c.SettingsCommands["showstack"]
c.SettingsCommands["togglebatch"] = c.SettingsCommands["batch"]
c.SettingsCommands["toggledebug"] = c.SettingsCommands["debug"]
c.SettingsCommands["toggleshowstack"] = c.SettingsCommands["showstack"]
c.ShowCommands["h"] = c.ShowCommands["history"]
c.ShowCommands["p"] = c.ShowCommands["dump"]
c.ShowCommands["v"] = c.ShowCommands["vars"]
c.StackCommands["c"] = c.StackCommands["clear"]
c.StackCommands["u"] = c.StackCommands["undo"]
}
// added to the command map:
func CommandSwap(c *Calc) {
if c.stack.Len() < 2 {
fmt.Println("stack too small, can't swap")
} else {
c.stack.Backup()
c.stack.Swap()
}
}
func CommandDup(c *Calc) {
item := c.stack.Last()
if len(item) == 1 {
c.stack.Backup()
c.stack.Push(item[0])
} else {
fmt.Println("stack empty")
}
}
func CommandEdit(calc *Calc) {
if calc.stack.Len() == 0 {
fmt.Println("empty stack")
return
}
calc.stack.Backup()
// put the stack contents into a tmp file
tmp, err := os.CreateTemp("", "stack")
if err != nil {
fmt.Println(err)
return
}
defer func() {
if err := os.Remove(tmp.Name()); err != nil {
log.Fatal(err)
}
}()
comment := `# add or remove numbers as you wish.
# each number must be on its own line.
# numbers must be floating point formatted.
`
_, err = tmp.WriteString(comment)
if err != nil {
fmt.Println(err)
return
}
for _, item := range calc.stack.All() {
_, err = fmt.Fprintf(tmp, "%f\n", item)
if err != nil {
fmt.Println(err)
return
}
}
if err := tmp.Close(); err != nil {
log.Fatal(err)
}
// determine which editor to use
editor := "vi"
enveditor, present := os.LookupEnv("EDITOR")
if present {
if editor != "" {
if _, err := os.Stat(editor); err == nil {
editor = enveditor
}
}
}
// execute editor with our tmp file containing current stack
cmd := exec.Command(editor, tmp.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
fmt.Println("could not run editor command: ", err)
return
}
// read the file back in
modified, err := os.Open(tmp.Name())
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer func() {
if err := modified.Close(); err != nil {
log.Fatal(err)
}
}()
// reset the stack
calc.stack.Clear()
// and put the new contents (if legit) back onto the stack
scanner := bufio.NewScanner(modified)
for scanner.Scan() {
line := strings.TrimSpace(calc.Comment.ReplaceAllString(scanner.Text(), ""))
if line == "" {
continue
}
num, err := strconv.ParseFloat(line, 64)
if err != nil {
fmt.Printf("%s is not a floating point number!\n", line)
continue
}
calc.stack.Push(num)
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading from file:", err)
}
}

View File

@@ -1,578 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"errors"
"math"
)
type Result struct {
Res float64
Err error
}
type Numbers []float64
type Function func(Numbers) Result
// every function we are able to call must be of type Funcall, which
// needs to specify how many numbers it expects and the actual go
// function to be executed.
//
// The function has to take a float slice as argument and return a
// float and an error object. The float slice is guaranteed to have
// the expected number of arguments.
//
// However, Lua functions are handled differently, see interpreter.go.
type Funcall struct {
Expectargs int // -1 means batch only mode, you'll get the whole stack as arg
Func Function
}
// will hold all hard coded functions and operators
type Funcalls map[string]*Funcall
// convenience function, create a new Funcall object, if expectargs
// was not specified, 2 is assumed.
func NewFuncall(function Function, expectargs ...int) *Funcall {
expect := 2
if len(expectargs) > 0 {
expect = expectargs[0]
}
return &Funcall{
Expectargs: expect,
Func: function,
}
}
// Convenience function, create new result
func NewResult(n float64, e error) Result {
return Result{Res: n, Err: e}
}
// the actual functions, called once during initialization.
func DefineFunctions() Funcalls {
funcmap := map[string]*Funcall{
// simple operators, they all expect 2 args
"+": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]+arg[1], nil)
},
),
"-": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]-arg[1], nil)
},
),
"x": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*arg[1], nil)
},
),
"/": NewFuncall(
func(arg Numbers) Result {
if arg[1] == 0 {
return NewResult(0, errors.New("division by null"))
}
return NewResult(arg[0]/arg[1], nil)
},
),
"^": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Pow(arg[0], arg[1]), nil)
},
),
"%": NewFuncall(
func(arg Numbers) Result {
return NewResult((arg[0]/100)*arg[1], nil)
},
),
"%-": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]-((arg[0]/100)*arg[1]), nil)
},
),
"%+": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]+((arg[0]/100)*arg[1]), nil)
},
),
"mod": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Remainder(arg[0], arg[1]), nil)
},
),
"sqrt": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Sqrt(arg[0]), nil)
},
1),
"abs": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Abs(arg[0]), nil)
},
1),
"acos": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Acos(arg[0]), nil)
},
1),
"acosh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Acosh(arg[0]), nil)
},
1),
"asin": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Asin(arg[0]), nil)
},
1),
"asinh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Asinh(arg[0]), nil)
},
1),
"atan": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Atan(arg[0]), nil)
},
1),
"atan2": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Atan2(arg[0], arg[1]), nil)
},
2),
"atanh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Atanh(arg[0]), nil)
},
1),
"cbrt": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Cbrt(arg[0]), nil)
},
1),
"ceil": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Ceil(arg[0]), nil)
},
1),
"cos": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Cos(arg[0]), nil)
},
1),
"cosh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Cosh(arg[0]), nil)
},
1),
"erf": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erf(arg[0]), nil)
},
1),
"erfc": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erfc(arg[0]), nil)
},
1),
"erfcinv": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erfcinv(arg[0]), nil)
},
1),
"erfinv": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erfinv(arg[0]), nil)
},
1),
"exp": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Exp(arg[0]), nil)
},
1),
"exp2": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Exp2(arg[0]), nil)
},
1),
"expm1": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Expm1(arg[0]), nil)
},
1),
"floor": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Floor(arg[0]), nil)
},
1),
"gamma": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Gamma(arg[0]), nil)
},
1),
"ilogb": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(math.Ilogb(arg[0])), nil)
},
1),
"j0": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.J0(arg[0]), nil)
},
1),
"j1": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.J1(arg[0]), nil)
},
1),
"log": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log(arg[0]), nil)
},
1),
"log10": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log10(arg[0]), nil)
},
1),
"log1p": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log1p(arg[0]), nil)
},
1),
"log2": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log2(arg[0]), nil)
},
1),
"logb": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Logb(arg[0]), nil)
},
1),
"pow": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Pow(arg[0], arg[1]), nil)
},
2),
"round": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Round(arg[0]), nil)
},
1),
"roundtoeven": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.RoundToEven(arg[0]), nil)
},
1),
"sin": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Sin(arg[0]), nil)
},
1),
"sinh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Sinh(arg[0]), nil)
},
1),
"tan": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Tan(arg[0]), nil)
},
1),
"tanh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Tanh(arg[0]), nil)
},
1),
"trunc": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Trunc(arg[0]), nil)
},
1),
"y0": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Y0(arg[0]), nil)
},
1),
"y1": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Y1(arg[0]), nil)
},
1),
"copysign": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Copysign(arg[0], arg[1]), nil)
},
2),
"dim": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Dim(arg[0], arg[1]), nil)
},
2),
"hypot": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Hypot(arg[0], arg[1]), nil)
},
2),
// converters of all kinds
"cm-to-inch": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/2.54, nil)
},
1),
"inch-to-cm": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*2.54, nil)
},
1),
"gallons-to-liters": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*3.785, nil)
},
1),
"liters-to-gallons": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/3.785, nil)
},
1),
"yards-to-meters": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*91.44, nil)
},
1),
"meters-to-yards": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/91.44, nil)
},
1),
"miles-to-kilometers": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*1.609, nil)
},
1),
"kilometers-to-miles": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1.609, nil)
},
1),
"bytes-to-kilobytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024, nil)
},
1),
"bytes-to-megabytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024/1024, nil)
},
1),
"bytes-to-gigabytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024/1024/1024, nil)
},
1),
"bytes-to-terabytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024/1024/1024/1024, nil)
},
1),
"or": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])|int(arg[1])), nil)
},
2),
"and": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])&int(arg[1])), nil)
},
2),
"xor": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])^int(arg[1])), nil)
},
2),
"<": NewFuncall(
func(arg Numbers) Result {
// Shift by negative number provibited, so check it.
// Note that we check against uint64 overflow as well here
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewResult(0, errors.New("negative shift amount"))
}
return NewResult(float64(int(arg[0])<<int(arg[1])), nil)
},
2),
">": NewFuncall(
func(arg Numbers) Result {
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewResult(0, errors.New("negative shift amount"))
}
return NewResult(float64(int(arg[0])>>int(arg[1])), nil)
},
2),
}
// aliases
funcmap["*"] = funcmap["x"]
funcmap["remainder"] = funcmap["mod"]
return funcmap
}
func DefineBatchFunctions() Funcalls {
funcmap := map[string]*Funcall{
"median": NewFuncall(
func(args Numbers) Result {
middle := len(args) / 2
return NewResult(args[middle], nil)
},
-1),
"mean": NewFuncall(
func(args Numbers) Result {
var sum float64
for _, item := range args {
sum += item
}
return NewResult(sum/float64(len(args)), nil)
},
-1),
"min": NewFuncall(
func(args Numbers) Result {
var min float64
min, args = args[0], args[1:]
for _, item := range args {
if item < min {
min = item
}
}
return NewResult(min, nil)
},
-1),
"max": NewFuncall(
func(args Numbers) Result {
var max float64
max, args = args[0], args[1:]
for _, item := range args {
if item > max {
max = item
}
}
return NewResult(max, nil)
},
-1),
"sum": NewFuncall(
func(args Numbers) Result {
var sum float64
for _, item := range args {
sum += item
}
return NewResult(sum, nil)
},
-1),
}
// aliases
funcmap["+"] = funcmap["sum"]
funcmap["avg"] = funcmap["mean"]
return funcmap
}

View File

@@ -1,180 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"errors"
"fmt"
lua "github.com/yuin/gopher-lua"
)
type Interpreter struct {
debug bool
script string
}
// LuaInterpreter is the lua interpreter, instantiated in main()
var LuaInterpreter *lua.LState
// holds a user provided lua function
type LuaFunction struct {
name string
help string
numargs int
}
// LuaFuncs must be global since init() is being called from lua which
// doesn't have access to the interpreter instance
var LuaFuncs map[string]LuaFunction
func NewInterpreter(script string, debug bool) *Interpreter {
return &Interpreter{debug: debug, script: script}
}
// initialize the lua environment properly
func (i *Interpreter) InitLua() {
// we only load a subset of lua Open modules and don't allow
// net, system or io stuff
for _, pair := range []struct {
n string
f lua.LGFunction
}{
{lua.LoadLibName, lua.OpenPackage},
{lua.BaseLibName, lua.OpenBase},
{lua.TabLibName, lua.OpenTable},
{lua.DebugLibName, lua.OpenDebug},
{lua.MathLibName, lua.OpenMath},
} {
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.NewFunction(pair.f),
NRet: 0,
Protect: true,
}, lua.LString(pair.n)); err != nil {
panic(err)
}
}
// load the lua config (which we expect to contain init() and math functions)
if err := LuaInterpreter.DoFile(i.script); err != nil {
panic(err)
}
// instantiate
LuaFuncs = map[string]LuaFunction{}
// that way the user can call register(...) from lua inside init()
LuaInterpreter.SetGlobal("register", LuaInterpreter.NewFunction(register))
// actually call init()
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal("init"),
NRet: 0,
Protect: true,
}); err != nil {
panic(err)
}
}
func (i *Interpreter) Debug(msg string) {
if i.debug {
fmt.Printf("DEBUG(lua): %s\n", msg)
}
}
func (i *Interpreter) FuncNumArgs(name string) int {
return LuaFuncs[name].numargs
}
// Call a user provided math function registered with register().
//
// Each function has to tell us how many args it expects, the actual
// function call from here is different depending on the number of
// arguments. 1 uses the last item of the stack, 2 the last two and -1
// all items (which translates to batch mode)
//
// The items array will be provided by calc.Eval(), these are
// non-popped stack items. So the items will only removed from the
// stack when the lua function execution is successful.
func (i *Interpreter) CallLuaFunc(funcname string, items []float64) (float64, error) {
i.Debug(fmt.Sprintf("calling lua func %s() with %d args",
funcname, LuaFuncs[funcname].numargs))
switch LuaFuncs[funcname].numargs {
case 0, 1:
// 1 arg variant
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1,
Protect: true,
}, lua.LNumber(items[0])); err != nil {
return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
}
case 2:
// 2 arg variant
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1,
Protect: true,
}, lua.LNumber(items[0]), lua.LNumber(items[1])); err != nil {
return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
}
case -1:
// batch variant, use lua table as array
table := LuaInterpreter.NewTable()
// put the whole stack into it
for _, item := range items {
table.Append(lua.LNumber(item))
}
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1,
Protect: true,
}, table); err != nil {
return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
}
}
// get result and cast to float64
if res, ok := LuaInterpreter.Get(-1).(lua.LNumber); ok {
LuaInterpreter.Pop(1)
return float64(res), nil
}
return 0, errors.New("function did not return a float64")
}
// called from lua to register a math function numargs may be 1, 2 or
// -1, it denotes the number of items from the stack requested by the
// lua function. -1 means batch mode, that is all items
func register(lstate *lua.LState) int {
function := lstate.ToString(1)
numargs := lstate.ToInt(2)
help := lstate.ToString(3)
LuaFuncs[function] = LuaFunction{
name: function,
numargs: numargs,
help: help,
}
return 1
}

View File

@@ -1,120 +0,0 @@
package cmd
// pager setup using bubbletea
// file shamlelessly copied from:
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.BorderStyle(b)
}()
)
type model struct {
content string
title string
ready bool
viewport viewport.Model
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !m.ready {
// Since this program is using the full size of the viewport we
// need to wait until we've received the window dimensions before
// we can initialize the viewport. The initial dimensions come in
// quickly, though asynchronously, which is why we wait for them
// here.
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
m.viewport.YPosition = headerHeight
m.viewport.SetContent(m.content)
m.ready = true
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight
}
}
// Handle keyboard and mouse events in the viewport
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) View() string {
if !m.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
func (m model) headerView() string {
// title := titleStyle.Render("RPN Help Overview")
title := titleStyle.Render(m.title)
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (m model) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func Pager(title, message string) {
p := tea.NewProgram(
model{content: message, title: title},
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
)
if _, err := p.Run(); err != nil {
fmt.Println("could not run pager:", err)
os.Exit(1)
}
}

View File

@@ -1,195 +0,0 @@
/*
Copyright © 2023-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"fmt"
"log"
"os"
"strings"
"github.com/chzyer/readline"
flag "github.com/spf13/pflag"
lua "github.com/yuin/gopher-lua"
)
const VERSION string = "2.1.7"
const Usage string = `This is rpn, a reverse polish notation calculator cli.
Usage: rpn [-bdvh] [<operator>]
Options:
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
Copyright (c) 2023-2025 T.v.Dein`
func Main() int {
calc := NewCalc()
showversion := false
showhelp := false
showmanual := false
enabledebug := false
configfile := ""
flag.BoolVarP(&calc.batch, "batchmode", "b", false, "batch mode")
flag.BoolVarP(&calc.showstack, "show-stack", "s", false, "show stack")
flag.BoolVarP(&calc.intermediate, "showin-termediate", "i", false,
"show intermediate results")
flag.BoolVarP(&enabledebug, "debug", "d", false, "debug mode")
flag.BoolVarP(&showversion, "version", "v", false, "show version")
flag.BoolVarP(&showhelp, "help", "h", false, "show usage")
flag.BoolVarP(&showmanual, "manual", "m", false, "show manual")
flag.StringVarP(&configfile, "config", "c",
os.Getenv("HOME")+"/.rpn.lua", "config file (lua format)")
flag.IntVarP(&calc.precision, "precision", "p", Precision, "floating point precision")
flag.Parse()
if showversion {
fmt.Printf("This is rpn version %s\n", VERSION)
return 0
}
if showhelp {
fmt.Println(Usage)
return 0
}
if enabledebug {
calc.ToggleDebug()
}
if showmanual {
man()
return 0
}
// the lua state object is global, instantiate it early
LuaInterpreter = lua.NewState(lua.Options{SkipOpenLibs: true})
defer LuaInterpreter.Close()
// our config file is interpreted as lua code, only functions can
// be defined, init() will be called by InitLua().
if _, err := os.Stat(configfile); err == nil {
luarunner := NewInterpreter(configfile, enabledebug)
luarunner.InitLua()
calc.SetInt(luarunner)
if calc.debug {
fmt.Println("loaded config")
}
} else if calc.debug {
fmt.Println(err)
}
if len(flag.Args()) > 1 {
// commandline calc operation, no readline etc needed
// called like rpn 2 2 +
calc.stdin = true
if err := calc.Eval(strings.Join(flag.Args(), " ")); err != nil {
fmt.Println(err)
return 1
}
return 0
}
// interactive mode, need readline
reader, err := readline.NewEx(&readline.Config{
Prompt: calc.Prompt(),
HistoryFile: os.Getenv("HOME") + "/.rpn-history",
HistoryLimit: 500,
AutoComplete: calc.completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
})
if err != nil {
panic(err)
}
defer func() {
if err := reader.Close(); err != nil {
log.Fatal(err)
}
}()
reader.CaptureExitSignal()
if inputIsStdin() {
// commands are coming on stdin, however we will still enter
// the same loop since readline just reads fine from stdin
calc.ToggleStdin()
}
for {
// primary program repl
line, err := reader.Readline()
if err != nil {
break
}
err = calc.Eval(line)
if err != nil {
fmt.Println(err)
}
reader.SetPrompt(calc.Prompt())
}
if len(flag.Args()) > 0 {
// called like this:
// echo 1 2 3 4 | rpn +
// batch mode enabled automatically
calc.batch = true
if err = calc.Eval(flag.Args()[0]); err != nil {
fmt.Println(err)
return 1
}
}
return 0
}
func inputIsStdin() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0
}
func man() {
Pager("rpn manual page", manpage)
}

View File

@@ -1,356 +0,0 @@
package cmd
var manpage = `
NAME
rpn - Programmable command-line calculator using reverse polish notation
SYNOPSIS
Usage: rpn [-bdvh] [<operator>]
Options:
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
DESCRIPTION
rpn is a command line calculator using reverse polish notation.
Working principle
Reverse Polish Notation (short: RPN) requires to have a stack where
numbers and results are being put. So, you put numbers onto the stack
and each math operation uses these for calculation, removes them and
puts the result back.
To visualize it, let's look at a calculation:
((80 + 20) / 2) * 4
This is how you enter the formula int an RPN calculator and how the
stack evolves during the operation:
| rpn commands | stack contents | calculation |
|--------------|----------------|---------------|
| 80 | 80 | |
| 20 | 80 20 | |
| + | 100 | 80 + 20 = 100 |
| 2 | 100 2 | |
| / | 50 | 100 / 2 = 50 |
| 4 | 50 4 | |
| x | 200 | 50 * 4 = 200 |
The last stack element 200 is the calculation result.
USAGE
The default mode of operation is the interactive mode. You'll get a
prompt which shows you the current size of the stack. At the prompt you
enter numbers followed by operators or mathematical functions. You can
use completion for the functions. You can either enter each number or
operator on its own line or separated by whitespace, that doesn't
matter. After a calculation the result will be immediately displayed
(and added to the stack). You can quit interactive mode using the
commands quit or exit or hit one of the "ctrl-d" or "ctrl-c" key
combinations.
If you feed data to standard input (STDIN), rpn just does the
calculation denoted in the contet fed in via stdin, prints the result
and exits. You can also specify a calculation on the commandline.
Here are the three variants ($ is the shell prompt):
$ rpn
rpn> 2
rpn> 2
rpn> +
= 4
$ rpn
rpn> 2 2 +
= 4
$ echo 2 2 + | rpn
4
$ rpn 2 2 +
4
The rpn calculator provides a batch mode which you can use to do math
operations on many numbers. Batch mode can be enabled using the
commandline option "-b" or toggled using the interactive command batch.
Not all math operations and functions work in batch mode though.
Example of batch mode usage:
$ rpn -b
rpn->batch > 2 2 2 2 +
= 8
$ rpn
rpn> batch
rpn->batch> 2 2 2 2 +
8
$ echo 2 2 2 2 + | rpn -b
8
$ echo 2 2 2 2 | rpn +
8
If the first parameter to rpn is a math operator or function, batch mode
is enabled automatically, see last example.
You can enter integers, floating point numbers (positive or negative) or
hex numbers (prefixed with 0x). Time values in hh::mm format are
possible as well.
STACK MANIPULATION
There are lots of stack manipulation commands provided. The most
important one is undo which goes back to the stack before the last math
operation.
You can use dump to display the stack. If debugging is enabled ("-d"
switch or debug toggle command), then the backup stack is also being
displayed.
The stack can be reversed using the reverse command. However, sometimes
only the last two values are in the wrong order. Use the swap command to
exchange them.
You can use the shift command to remove the last number from the stack.
BUILTIN OPERATORS AND FUNCTIONS
Basic operators:
+ add
- subtract
/ divide
x multiply (alias: *)
^ power
Bitwise operators:
and bitwise and
or bitwise or
xor bitwise xor
< left shift
> right shift
Percent functions:
% percent
%- subtract percent
%+ add percent
Batch functions:
sum sum of all values (alias: +)
max max of all values
min min of all values
mean mean of all values (alias: avg)
median median of all values
Math functions:
mod sqrt abs acos acosh asin asinh atan atan2 atanh cbrt ceil cos cosh
erf erfc erfcinv erfinv exp exp2 expm1 floor gamma ilogb j0 j1 log
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot
Conversion functions:
cm-to-inch yards-to-meters bytes-to-kilobytes
inch-to-cm meters-to-yards bytes-to-megabytes
gallons-to-liters miles-to-kilometers bytes-to-gigabytes
liters-to-gallons kilometers-to-miles bytes-to-terabytes
Configuration Commands:
[no]batch toggle batch mode (nobatch turns it off)
[no]debug toggle debug output (nodebug turns it off)
[no]showstack show the last 5 items of the stack (noshowtack turns it off)
Show commands:
dump display the stack contents
hex show last stack item in hex form (converted to int)
history display calculation history
vars show list of variables
Stack manipulation commands:
clear clear the whole stack
shift remove the last element of the stack
reverse reverse the stack elements
swap exchange the last two stack elements
dup duplicate last stack item
undo undo last operation
edit edit the stack interactively using vi or $EDITOR
Other commands:
help|? show this message
manual show manual
quit|exit|c-d|c-c exit program
Register variables:
>NAME Put last stack element into variable NAME
<NAME Retrieve variable NAME and put onto stack
Refer to https://pkg.go.dev/math for details about those functions.
There are also a number of shortcuts for some commands available:
d debug
b batch
s showstack
h history
p dump (aka print)
v vars
c clear
u undo
INTERACTIVE REPL
While you can use rpn in the command-line, the best experience you'll
have is the interactive repl (read eval print loop). Just execute "rpn"
and you'll be there.
In interactive mode you can use TAB completion to complete commands,
operators and functions. There's also a history, which allows you to
repeat complicated calculations (as long as you've entered them in one
line).
There are also a lot of key bindings, here are the most important ones:
ctrl-c + ctrl-d
Exit interactive rpn
ctrl-z
Send rpn to the backgound.
ctrl-a
Beginning of line.
ctrl-e
End of line.
ctrl-l
Clear the screen.
ctrl-r
Search through history.
COMMENTS
Lines starting with "#" are being ignored as comments. You can also
append comments to rpn input, e.g.:
# a comment
123 # another comment
In this case only 123 will be added to the stack.
VARIABLES
You can register the last item of the stack into a variable. Variable
names must be all caps. Use the ">NAME" command to put a value into
variable "NAME". Use "<NAME" to retrieve the value of variable "NAME"
and put it onto the stack.
The command vars can be used to get a list of all variables.
EXTENDING RPN USING LUA
You can use a lua script with lua functions to extend the calculator. By
default the tool looks for "~/.rpn.lua". You can also specify a script
using the <kbd>-c</kbd> flag.
Here's an example of such a script:
function add(a,b)
return a + b
end
function init()
register("add", 2, "addition")
end
Here we created a function "add()" which adds two parameters. All
parameters are "FLOAT64" numbers. You don't have to worry about stack
management, this is taken care of automatically.
The function "init()" MUST be defined, it will be called on startup. You
can do anything you like in there, but you need to call the "register()"
function to register your functions to the calculator. This function
takes these parameters:
* function name
* number of arguments expected (see below)
Number of expected arguments can be:
- 0: expect 1 argument but do NOT modify the stack
- 1-n: do a singular calculation
- -1: batch mode work with all numbers on the stack
* help text
Please refer to the lua language reference:
<https://www.lua.org/manual/5.4/> for more details about LUA.
Please note, that io, networking and system stuff is not allowed though.
So you can't open files, execute other programs or open a connection to
the outside!
CONFIGURATION
rpn can be configured via command line flags (see usage above). Most of
the flags are also available as interactive commands, such as "--batch"
has the same effect as the batch command.
The floating point number precision option "-p, --precision" however is
not available as interactive command, it MUST be configured on the
command line, if needed. The default precision is 2.
GETTING HELP
In interactive mode you can enter the help command (or ?) to get a short
help along with a list of all supported operators and functions.
To read the manual you can use the manual command in interactive mode.
The commandline option "-m" does the same thing.
If you have installed rpn as a package or using the distributed tarball,
there will also be a manual page you can read using "man rpn".
BUGS
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:
<https://codeberg.org/scip/rpnc/issues>.
LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3.
Copyright (c) 2023-2024 by Thomas von Dein
This software uses the following GO modules:
readline (github.com/chzyer/readline)
Released under the MIT License, Copyright (c) 2016-2023 ChenYe
pflag (https://github.com/spf13/pflag)
Released under the BSD 3 license, Copyright 2013-2023 Steve Francia
gopher-lua (github.com/yuin/gopher-lua)
Released under the MIT License, Copyright (c) 2015-2023 Yusuke
Inuzuka
AUTHORS
Thomas von Dein tom AT vondein DOT org
`

View File

@@ -1,251 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"container/list"
"fmt"
"sync"
)
// The stack uses a linked list provided by container/list as storage
// and works after the LIFO principle (last in first out). Most of the
// work is being done in the linked list, but we add a couple of
// cenvenient functions, so that the user doesn't have to cope with
// list directly.
type Stack struct {
linklist list.List
backup list.List
debug bool
rev int
backuprev int
mutex sync.Mutex
}
// FIXME: maybe use a separate stack object for backup so that it has
// its own revision etc
func NewStack() *Stack {
return &Stack{
linklist: list.List{},
backup: list.List{},
rev: 0,
backuprev: 0,
}
}
func (s *Stack) Debug(msg string) {
if s.debug {
fmt.Printf("DEBUG(%03d): %s\n", s.rev, msg)
}
}
func (s *Stack) ToggleDebug() {
s.debug = !s.debug
}
func (s *Stack) Bump() {
s.rev++
}
// append an item to the stack
func (s *Stack) Push(item float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.Debug(fmt.Sprintf(" push to stack: %.2f", item))
s.Bump()
s.linklist.PushBack(item)
}
// remove and return an item from the stack
func (s *Stack) Pop() float64 {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.linklist.Len() == 0 {
return 0
}
tail := s.linklist.Back()
val := tail.Value
s.linklist.Remove(tail)
s.Debug(fmt.Sprintf(" remove from stack: %.2f", val))
s.Bump()
return val.(float64)
}
// just remove the last item, do not return it
func (s *Stack) Shift(num ...int) {
s.mutex.Lock()
defer s.mutex.Unlock()
count := 1
if len(num) > 0 {
count = num[0]
}
if s.linklist.Len() == 0 {
return
}
for i := 0; i < count; i++ {
tail := s.linklist.Back()
s.linklist.Remove(tail)
s.Debug(fmt.Sprintf("remove from stack: %.2f", tail.Value))
}
}
func (s *Stack) Swap() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.linklist.Len() < 2 {
return
}
prevA := s.linklist.Back()
s.linklist.Remove(prevA)
prevB := s.linklist.Back()
s.linklist.Remove(prevB)
s.Debug(fmt.Sprintf("swapping %.2f with %.2f", prevB.Value, prevA.Value))
s.linklist.PushBack(prevA.Value)
s.linklist.PushBack(prevB.Value)
}
// Return the last num items from the stack w/o modifying it.
func (s *Stack) Last(num ...int) []float64 {
items := []float64{}
stacklen := s.Len()
count := 1
if len(num) > 0 {
count = num[0]
}
for e := s.linklist.Front(); e != nil; e = e.Next() {
if stacklen <= count {
items = append(items, e.Value.(float64))
}
stacklen--
}
return items
}
// Return all elements of the stack without modifying it.
func (s *Stack) All() []float64 {
items := []float64{}
for e := s.linklist.Front(); e != nil; e = e.Next() {
items = append(items, e.Value.(float64))
}
return items
}
// dump the stack to stdout, including backup if debug is enabled
func (s *Stack) Dump() {
fmt.Printf("Stack revision %d (%p):\n", s.rev, &s.linklist)
for e := s.linklist.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
if s.debug {
fmt.Printf("Backup stack revision %d (%p):\n", s.backuprev, &s.backup)
for e := s.backup.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}
}
func (s *Stack) Clear() {
s.Debug("clearing stack")
s.linklist = list.List{}
}
func (s *Stack) Len() int {
return s.linklist.Len()
}
func (s *Stack) Backup() {
// we need clean the list and restore it from scratch each time we
// make a backup, because the elements in list.List{} are pointers
// and lead to unexpected results. The methid here works reliably
// at least.
s.mutex.Lock()
defer s.mutex.Unlock()
s.Debug(fmt.Sprintf("backing up %d items from rev %d",
s.linklist.Len(), s.rev))
s.backup = list.List{}
for e := s.linklist.Front(); e != nil; e = e.Next() {
s.backup.PushBack(e.Value.(float64))
}
s.backuprev = s.rev
}
func (s *Stack) Restore() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.rev == 0 {
fmt.Println("error: stack is empty.")
return
}
s.Debug(fmt.Sprintf("restoring stack to revision %d", s.backuprev))
s.rev = s.backuprev
s.linklist = list.List{}
for e := s.backup.Front(); e != nil; e = e.Next() {
s.linklist.PushBack(e.Value.(float64))
}
}
func (s *Stack) Reverse() {
s.mutex.Lock()
defer s.mutex.Unlock()
items := []float64{}
for e := s.linklist.Front(); e != nil; e = e.Next() {
tail := s.linklist.Back()
items = append(items, tail.Value.(float64))
s.linklist.Remove(tail)
}
for i := len(items) - 1; i >= 0; i-- {
s.linklist.PushFront(items[i])
}
}

View File

@@ -1,190 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"testing"
)
func TestPush(t *testing.T) {
t.Run("push", func(t *testing.T) {
s := NewStack()
s.Push(5)
if s.linklist.Back().Value != 5.0 {
t.Errorf("push failed:\n+++ got: %f\n--- want: %f",
s.linklist.Back().Value, 5.0)
}
})
}
func TestPop(t *testing.T) {
t.Run("pop", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
got := stack.Pop()
if got != 5.0 {
t.Errorf("pop failed:\n+++ got: %f\n--- want: %f",
got, 5.0)
}
if stack.Len() != 0 {
t.Errorf("stack not empty after pop()")
}
})
}
func TestPops(t *testing.T) {
t.Run("pops", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
stack.Push(5)
stack.Push(5)
stack.Pop()
if stack.Len() != 2 {
t.Errorf("stack len not correct after pop:\n+++ got: %d\n--- want: %d",
stack.Len(), 2)
}
})
}
func TestShift(t *testing.T) {
t.Run("shift", func(t *testing.T) {
stack := NewStack()
stack.Shift()
if stack.Len() != 0 {
t.Errorf("stack not empty after shift()")
}
})
}
func TestClear(t *testing.T) {
t.Run("clear", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
stack.Push(5)
stack.Push(5)
stack.Clear()
if stack.Len() != 0 {
t.Errorf("stack not empty after clear()")
}
})
}
func TestLast(t *testing.T) {
t.Run("last", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
got := stack.Last()
if len(got) != 1 {
t.Errorf("last failed:\n+++ got: %d elements\n--- want: %d elements",
len(got), 1)
}
if got[0] != 5.0 {
t.Errorf("last failed:\n+++ got: %f\n--- want: %f",
got, 5.0)
}
if stack.Len() != 1 {
t.Errorf("stack modified after last()")
}
})
}
func TestAll(t *testing.T) {
t.Run("all", func(t *testing.T) {
stack := NewStack()
list := []float64{2, 4, 6, 8}
for _, item := range list {
stack.Push(item)
}
got := stack.All()
if len(got) != len(list) {
t.Errorf("all failed:\n+++ got: %d elements\n--- want: %d elements",
len(got), len(list))
}
for i := 1; i < len(list); i++ {
if got[i] != list[i] {
t.Errorf("all failed (element %d):\n+++ got: %f\n--- want: %f",
i, got[i], list[i])
}
}
if stack.Len() != len(list) {
t.Errorf("stack modified after last()")
}
})
}
func TestBackupRestore(t *testing.T) {
t.Run("shift", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
stack.Backup()
stack.Clear()
stack.Restore()
if stack.Len() != 1 {
t.Errorf("stack not correctly restored()")
}
value := stack.Pop()
if value != 5.0 {
t.Errorf("stack not identical to old revision:\n+++ got: %f\n--- want: %f",
value, 5.0)
}
})
}
func TestReverse(t *testing.T) {
t.Run("reverse", func(t *testing.T) {
stack := NewStack()
list := []float64{2, 4, 6}
reverse := []float64{6, 4, 2}
for _, item := range list {
stack.Push(item)
}
stack.Reverse()
got := stack.All()
if len(got) != len(list) {
t.Errorf("all failed:\n+++ got: %d elements\n--- want: %d elements",
len(got), len(list))
}
for i := 1; i < len(reverse); i++ {
if got[i] != reverse[i] {
t.Errorf("reverse failed (element %d):\n+++ got: %f\n--- want: %f",
i, got[i], list[i])
}
}
})
}

View File

@@ -1,79 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"fmt"
"math"
"strings"
)
// find an item in a list, generic variant
func contains[E comparable](s []E, v E) bool {
for _, vs := range s {
if v == vs {
return true
}
}
return false
}
// look if a key in a map exists, generic variant
func exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false
}
func const2num(name string) float64 {
switch name {
case "Pi":
return math.Pi
case "Phi":
return math.Phi
case "Sqrt2":
return math.Sqrt2
case "SqrtE":
return math.SqrtE
case "SqrtPi":
return math.SqrtPi
case "SqrtPhi":
return math.SqrtPhi
case "Ln2":
return math.Ln2
case "Log2E":
return math.Log2E
case "Ln10":
return math.Ln10
case "Log10E":
return math.Log10E
default:
return 0
}
}
func list2str(list Numbers) string {
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]")
}
func Error(m string) error {
return fmt.Errorf("Error: %s", m)
}

View File

@@ -1,32 +0,0 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"testing"
)
func TestContains(t *testing.T) {
list := []string{"a", "b", "c"}
t.Run("contains", func(t *testing.T) {
if !contains(list, "a") {
t.Errorf("a in [a,b,c] not found")
}
})
}

View File

@@ -1,38 +0,0 @@
-- simple function, return the lower number of the two operands
function lower(a,b)
if a < b then
return a
else
return b
end
end
-- calculate parallel resistance. Batch function (registered with -1,
-- see below). Takes a table as parameter.
--
-- Formula: 1/( (1/R1) + (1/R2) + ...)
function parallelresistance(list)
sumres = 0
for i, value in ipairs(list) do
sumres = sumres + 1 / value
end
return 1 / sumres
end
-- converter example
function inch2centimeter(inches)
return inches * 2.54
end
function init()
-- expects 2 args
register("lower", 2, "lower")
-- expects a list of all numbers on the stack, batch mode
register("parallelresistance", -1, "parallel resistance")
-- expects 1 arg, but doesn't pop()
register("inch2centimeter", 0)
end

34
go.mod
View File

@@ -1,34 +0,0 @@
module rpn
go 1.24.5
require (
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/chzyer/readline v1.5.1
github.com/rogpeppe/go-internal v1.14.1
github.com/spf13/pflag v1.0.10
github.com/yuin/gopher-lua v1.1.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/tools v0.26.0 // indirect
)

60
go.sum
View File

@@ -1,60 +0,0 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=

27
main.go
View File

@@ -1,27 +0,0 @@
/*
Copyright © 2023-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"os"
"rpn/cmd"
)
func main() {
os.Exit(cmd.Main())
}

View File

@@ -1,19 +0,0 @@
package main
import (
"testing"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
testscript.Main(m, map[string]func(){
"rpn": main,
})
}
func TestRpn(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "t",
})
}

400
rpn.pod
View File

@@ -1,400 +0,0 @@
=head1 NAME
rpn - Programmable command-line calculator using reverse polish notation
=head1 SYNOPSIS
Usage: rpn [-bdvh] [<operator>]
Options:
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
=head1 DESCRIPTION
rpn is a command line calculator using reverse polish notation.
=head2 Working principle
Reverse Polish Notation (short: RPN) requires to have a stack where
numbers and results are being put. So, you put numbers onto the stack
and each math operation uses these for calculation, removes them and
puts the result back.
To visualize it, let's look at a calculation:
((80 + 20) / 2) * 4
This is how you enter the formula int an RPN calculator and how the
stack evolves during the operation:
| rpn commands | stack contents | calculation |
|--------------|----------------|---------------|
| 80 | 80 | |
| 20 | 80 20 | |
| + | 100 | 80 + 20 = 100 |
| 2 | 100 2 | |
| / | 50 | 100 / 2 = 50 |
| 4 | 50 4 | |
| x | 200 | 50 * 4 = 200 |
The last stack element 200 is the calculation result.
=head2 USAGE
The default mode of operation is the interactive mode. You'll get a
prompt which shows you the current size of the stack. At the prompt
you enter numbers followed by operators or mathematical functions. You
can use completion for the functions. You can either enter each number
or operator on its own line or separated by whitespace, that doesn't
matter. After a calculation the result will be immediately displayed
(and added to the stack). You can quit interactive mode using the
commands B<quit> or B<exit> or hit one of the C<ctrl-d> or C<ctrl-c>
key combinations.
If you feed data to standard input (STDIN), rpn just does the
calculation denoted in the contet fed in via stdin, prints the result
and exits. You can also specify a calculation on the commandline.
Here are the three variants ($ is the shell prompt):
$ rpn
rpn> 2
rpn> 2
rpn> +
= 4
$ rpn
rpn> 2 2 +
= 4
$ echo 2 2 + | rpn
4
$ rpn 2 2 +
4
The rpn calculator provides a batch mode which you can use to do math
operations on many numbers. Batch mode can be enabled using the
commandline option C<-b> or toggled using the interactive command
B<batch>. Not all math operations and functions work in batch mode
though.
Example of batch mode usage:
$ rpn -b
rpn->batch > 2 2 2 2 +
= 8
$ rpn
rpn> batch
rpn->batch> 2 2 2 2 +
8
$ echo 2 2 2 2 + | rpn -b
8
$ echo 2 2 2 2 | rpn +
8
If the first parameter to rpn is a math operator or function, batch
mode is enabled automatically, see last example.
You can enter integers, floating point numbers (positive or negative)
or hex numbers (prefixed with 0x). Time values in hh::mm format are
possible as well.
=head2 STACK MANIPULATION
There are lots of stack manipulation commands provided. The most
important one is B<undo> which goes back to the stack before the last
math operation.
You can use B<dump> to display the stack. If debugging
is enabled (C<-d> switch or B<debug> toggle command), then the backup
stack is also being displayed.
The stack can be reversed using the B<reverse> command. However,
sometimes only the last two values are in the wrong order. Use the
B<swap> command to exchange them.
You can use the B<shift> command to remove the last number from the
stack.
=head2 BUILTIN OPERATORS AND FUNCTIONS
Basic operators:
+ add
- subtract
/ divide
x multiply (alias: *)
^ power
Bitwise operators:
and bitwise and
or bitwise or
xor bitwise xor
< left shift
> right shift
Percent functions:
% percent
%- subtract percent
%+ add percent
Batch functions:
sum sum of all values (alias: +)
max max of all values
min min of all values
mean mean of all values (alias: avg)
median median of all values
Math functions:
mod sqrt abs acos acosh asin asinh atan atan2 atanh cbrt ceil cos cosh
erf erfc erfcinv erfinv exp exp2 expm1 floor gamma ilogb j0 j1 log
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot
Conversion functions:
cm-to-inch yards-to-meters bytes-to-kilobytes
inch-to-cm meters-to-yards bytes-to-megabytes
gallons-to-liters miles-to-kilometers bytes-to-gigabytes
liters-to-gallons kilometers-to-miles bytes-to-terabytes
Configuration Commands:
[no]batch toggle batch mode (nobatch turns it off)
[no]debug toggle debug output (nodebug turns it off)
[no]showstack show the last 5 items of the stack (noshowtack turns it off)
Show commands:
dump display the stack contents
hex show last stack item in hex form (converted to int)
history display calculation history
vars show list of variables
Stack manipulation commands:
clear clear the whole stack
shift remove the last element of the stack
reverse reverse the stack elements
swap exchange the last two stack elements
dup duplicate last stack item
undo undo last operation
edit edit the stack interactively using vi or $EDITOR
Other commands:
help|? show this message
manual show manual
quit|exit|c-d|c-c exit program
Register variables:
>NAME Put last stack element into variable NAME
<NAME Retrieve variable NAME and put onto stack
Refer to https://pkg.go.dev/math for details about those functions.
There are also a number of shortcuts for some commands available:
d debug
b batch
s showstack
h history
p dump (aka print)
v vars
c clear
u undo
=head1 INTERACTIVE REPL
While you can use rpn in the command-line, the best experience you'll
have is the interactive repl (read eval print loop). Just execute
C<rpn> and you'll be there.
In interactive mode you can use TAB completion to complete commands,
operators and functions. There's also a history, which allows you to
repeat complicated calculations (as long as you've entered them in one
line).
There are also a lot of key bindings, here are the most important
ones:
=over
=item ctrl-c + ctrl-d
Exit interactive rpn
=item ctrl-z
Send rpn to the backgound.
=item ctrl-a
Beginning of line.
=item ctrl-e
End of line.
=item ctrl-l
Clear the screen.
=item ctrl-r
Search through history.
=back
=head1 COMMENTS
Lines starting with C<#> are being ignored as comments. You can also
append comments to rpn input, e.g.:
# a comment
123 # another comment
In this case only 123 will be added to the stack.
=head1 VARIABLES
You can register the last item of the stack into a variable. Variable
names must be all caps. Use the ">NAME" command to put a value into
variable "NAME". Use "<NAME" to retrieve the value of variable "NAME"
and put it onto the stack.
The command B<vars> can be used to get a list of all variables.
=head1 EXTENDING RPN USING LUA
You can use a lua script with lua functions to extend the
calculator. By default the tool looks for C<~/.rpn.lua>. You can also
specify a script using the <kbd>-c</kbd> flag.
Here's an example of such a script:
function add(a,b)
return a + b
end
function init()
register("add", 2, "addition")
end
Here we created a function C<add()> which adds two parameters. All
parameters are C<FLOAT64> numbers. You don't have to worry about stack
management, this is taken care of automatically.
The function C<init()> B<MUST> be defined, it will be called on
startup. You can do anything you like in there, but you need to call
the C<register()> function to register your functions to the
calculator. This function takes these parameters:
=over
=item *
function name
=item *
number of arguments expected (see below)
Number of expected arguments can be:
- 0: expect 1 argument but do NOT modify the stack
- 1-n: do a singular calculation
- -1: batch mode work with all numbers on the stack
=item *
help text
=back
Please refer to the lua language reference:
L<https://www.lua.org/manual/5.4/> for more details about LUA.
B<Please note, that io, networking and system stuff is not allowed
though. So you can't open files, execute other programs or open a
connection to the outside!>
=head1 CONFIGURATION
B<rpn> can be configured via command line flags (see usage
above). Most of the flags are also available as interactive commands,
such as C<--batch> has the same effect as the B<batch> command.
The floating point number precision option C<-p, --precision> however
is not available as interactive command, it MUST be configured on the
command line, if needed. The default precision is 2.
=head1 GETTING HELP
In interactive mode you can enter the B<help> command (or B<?>) to get
a short help along with a list of all supported operators and
functions.
To read the manual you can use the B<manual> command in interactive
mode. The commandline option C<-m> does the same thing.
If you have installed rpn as a package or using the distributed
tarball, there will also be a manual page you can read using C<man rpn>.
=head1 BUGS
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
L<https://codeberg.org/scip/rpnc/issues>.
=head1 LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2023-2024 by Thomas von Dein
This software uses the following GO modules:
=over 4
=item readline (github.com/chzyer/readline)
Released under the MIT License, Copyright (c) 2016-2023 ChenYe
=item pflag (https://github.com/spf13/pflag)
Released under the BSD 3 license, Copyright 2013-2023 Steve Francia
=item gopher-lua (github.com/yuin/gopher-lua)
Released under the MIT License, Copyright (c) 2015-2023 Yusuke Inuzuka
=back
=head1 AUTHORS
Thomas von Dein B<tom AT vondein DOT org>
=cut

BIN
rpnc.mp4

Binary file not shown.

View File

@@ -1,2 +0,0 @@
exec rpn 1 2 dump
stdout 'Stack revision 2 .0x'

View File

@@ -1,2 +0,0 @@
! exec rpn 1 2 dumb
stdout 'unknown command or operator'

View File

@@ -1,2 +0,0 @@
exec rpn -p 4 2 3 /
stdout '0.6667\n'

View File

@@ -1,2 +0,0 @@
! exec rpn 4 +
stdout 'stack doesn''t provide enough arguments'

View File

@@ -1,2 +0,0 @@
exec rpn -d 44 55 *
stdout 'push to stack: 2420.00\n'

View File

@@ -1,2 +0,0 @@
! exec rpn 100 50 50 - /
stdout 'division by null'

View File

@@ -1,16 +0,0 @@
exec rpn -d -c test.lua 3 5 lower
stdout '3\n'
-- test.lua --
function lower(a,b)
if a < b then
return a
else
return b
end
end
function init()
-- expects 2 args
register("lower", 2, "lower")
end

View File

@@ -1,2 +0,0 @@
exec rpn 09:55 4:15 -
stdout '5.67\n'

View File

@@ -1,2 +0,0 @@
exec rpn 44 55 *
stdout '2420\n'

View File

@@ -1,2 +0,0 @@
exec rpn -h
stdout 'This is rpn'

View File

@@ -1,2 +0,0 @@
exec rpn -v
stdout 'This is rpn version'

View File

@@ -1,4 +0,0 @@
exec echo 1 2 3 4 5 batch median
stdin stdout
exec rpn
[unix] stdout '3\n'

View File

@@ -1,4 +0,0 @@
exec echo 1 2 3 4 5
stdin stdout
[unix] exec rpn median
[unix] stdout '3\n'

View File

@@ -1,4 +0,0 @@
exec echo 10 10 +
stdin stdout
exec rpn
[unix] stdout '20\n'

View File

@@ -1,13 +0,0 @@
stdin input.txt
exec rpn
[unix] stdout '28\n'
-- input.txt --
10
10
+
>SUM
clear
8
<SUM
+

View File

@@ -1,4 +0,0 @@
exec echo 1 2 3 4 5 median
stdin stdout
exec rpn -b
[unix] stdout '3\n'

View File

@@ -1,13 +0,0 @@
-- simple function, return the lower number of the two operands
function lower(a,b)
if a < b then
return a
else
return b
end
end
function init()
-- expects 2 args
register("lower", 2, "lower")
end