Added a core namespace with a -main method; upversioned.

Also, Interactive read-eval-print loop, debugging, and other minor enhancements.
This commit is contained in:
Simon Brooke 2024-06-08 14:49:55 +01:00
parent f2a48cbe04
commit 4d3c36959c
9 changed files with 444 additions and 90 deletions

31
.gitignore vendored Normal file
View file

@ -0,0 +1,31 @@
/target
/classes
/checkouts
resources/scratch/
profiles.clj
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/
.idea/
*~
.calva/
.clj-kondo/
.lsp/
resources/scratch.lsp
Sysout*.lsp
*.pdf
src/beowulf/scratch.clj
.portal/vs-code.edn
.portal/
.classpath
.project
.vscode

View file

@ -22,11 +22,64 @@ There are solutions to katas very similar to this one on the 'net. Please don't
## Usage
FIXME
Substantially rewritten for version 0.2.0, Antonine now has an interactive mode. You can download it as a jar file from here, and invoke the jar file:
```bash
java -jar antonine-0.2.0-standalone.jar
```
The following command line options are recognised:
```
-b, --banner BANNER ANTONIVS ORNARE | SIMON RIVVLVS HOC FECIT | MMXXIV The welcome banner to display
-p, --prompt PROMPT COMPVTARE | The prompt to display
-s, --stop-word WORD FINIS The stop word to use at the end of a session
-v, --verbosity LEVEL 0 Verbosity level
```
Verbosity levels as follows
1. shows the arabic rendition of the value as well as the roman rendition;
2. additionally also shows the parse tree of the input;
3. currently not used;
4. currently not used;
5. additionally shows the parse of the command line options and arguments.
An expression to evaluate may be passed as command line arguments, in which case options are ignored, that expression is evaluated, the result printed, and the program exits with an exit status of zero:
```
simon@mason:~/workspace/antonine$ java -jar target/antonine-0.2.0-standalone.jar MMMD / XVI
CCXVIII
```
Otherwise, Antonine will print its banner and enter a read-eval-print loop, exiting with a status of 0 when
1. The `stop-word`, by default `FINIS`, is entered as an input line; or
2. A blank input line is entered.
Thus:
```
simon@mason:~/workspace/antonine$ java -jar target/antonine-0.2.0-standalone.jar
ANTONIVS ORNARE | SIMON RIVVLVS HOC FECIT | MMXXIV
COMPVTARE | MMMD / XIV
CCL
COMPVTARE | iii + iv
VII
COMPVTARE | iii + iv * v
XXIII
COMPVTARE | IIII + IV
VIII
COMPVTARE | FINIS
VALE
simon@mason:~/workspace/antonine$
```
## License
Copyright © 2015 Simon Brooke (simon@journeyman.cc)
Copyright © MMXV-MMXXIV Simon Brooke (simon@journeyman.cc)
Distributed under the GNU General Public License either version 2.0 or (at
your option) any later version.

77
doc/intro.md Normal file
View file

@ -0,0 +1,77 @@
# Introduction to antonine
A calculator to aid military quantity surveyors
## Brief
Right, Troops!
You're appointed to the engineering staff for the project of building the Antonine wall; specifically you will be quantity surveyors. Unfortunately, it is CXLII AD, and Arabic numerals have not yet been invented.
In order to do your work, you will need a calculator. The calculator must accept input in Roman numerals, and produce output in Roman numerals.
The wall is to be XXXXII Roman miles long, and will be protected by XVI towers. You'll be relieved to know that the wall is to be build mainly of turf, which you can cut locally, and the walls of the towers can be built with local fieldstone. But each tower will need LXIV masoned corner stones.
How many corner stones do you need altogether?
You have MMMD roof tiles. How many roof tiles can you allocate to each tower? If each tower needs DC roof tiles, how many towers will you have to thatch?
To write your calculator you may use any language you like (Latin is not mandatory). However, if your language already has libraries to parse and print Roman numerals, you may not use those libraries (you may use any other libraries; you may specifically use parser generators). You should allow addition, subtraction, multiplication and division, but you aren't required to handle fractions. You may expect well formed input (i.e. the input will be valid Roman numerals), but for extra credit you could detect and reject ill formed input. User interface is optional.
There are solutions to katas very similar to this one on the 'net. Please don't search for these before the session - it will spoil the fun. Looking up the rules for Roman numerals on Wikipedia or other sites is completely legitimate, provided you don't look at code.
## Usage
Substantially rewritten for version 0.2.0, Antonine now has an interactive mode. You can download it as a jar file from here, and invoke the jar file:
```bash
java -jar antonine-0.2.0-standalone.jar
```
The following command line options are recognised:
```
-b, --banner BANNER ANTONIVS ORNARE | SIMON RIVVLVS HOC FECIT | MMXXIV The welcome banner to display
-p, --prompt PROMPT COMPVTARE | The prompt to display
-s, --stop-word WORD FINIS The stop word to use at the end of a session
-v, --verbosity LEVEL 0 Verbosity level
```
An expression to evaluate may be passed as command line arguments, in which case options are ignored, that expression is evaluated, the result printed, and the program exits with an exit status of zero:
```
simon@mason:~/workspace/antonine$ java -jar target/antonine-0.2.0-standalone.jar MMMD / XVI
CCXVIII
```
Otherwise, Antonine will print its banner and enter a read-eval-print loop, exiting with a status of 0 when
1. The `stop-word`, by default `FINIS`, is entered as an input line; or
2. A blank input line is entered.
Thus:
```
simon@mason:~/workspace/antonine$ java -jar target/antonine-0.2.0-standalone.jar
ANTONIVS ORNARE | SIMON RIVVLVS HOC FECIT | MMXXIV
COMPVTARE | MMMD / XIV
CCL
COMPVTARE | iii + iv
VII
COMPVTARE | iii + iv * v
XXIII
COMPVTARE | IIII + IV
VIII
COMPVTARE | FINIS
VALE
simon@mason:~/workspace/antonine$
```
## License
Copyright © MMXV-MMXXIV Simon Brooke (simon@journeyman.cc)
Distributed under the GNU General Public License either version 2.0 or (at
your option) any later version.

View file

@ -1,6 +1,15 @@
(defproject antonine "0.1.0-SNAPSHOT"
(defproject antonine "0.2.0"
:aot :all
:description "A calculator which uses Roman numerals."
:url "http://example.com/FIXME"
:license {:name "GNU General Public License"
:url "http://www.gnu.org/licenses/gpl-2.0.html"}
:dependencies [[org.clojure/clojure "1.6.0"]])
:main antonine.core
:dependencies [[instaparse "1.5.0"]
[org.clojure/clojure "1.11.3"]
[org.clojure/tools.cli "1.1.230"]
[org.jline/jline "3.23.0"]]
:plugins [[lein-cljsbuild "1.1.8"]]
:profiles {:jar {:aot :all}
:uberjar {:aot :all}}
)

8
resources/grammar.bnf Normal file
View file

@ -0,0 +1,8 @@
EXPRESSION := NUMBER | NUMBER <SPACE> OPERATOR <SPACE> EXPRESSION
<NUMBER> := #'[IVXLCDM]+'
<OPERATOR> := ADD | SUBTRACT | MULTIPLY | DIVIDE
ADD := <'+'>
SUBTRACT := <'-'>
MULTIPLY := <'x'> | <'*'>
DIVIDE := <'/'>
SPACE := #'(?U)\s+'

View file

@ -1,86 +0,0 @@
(ns antonine.calculator
(:require [clojure.string :only [split trim triml]]))
(defn incordec
"In reading roman numerals, a lower value character preceding a higher value may imply a decrement
to the higher value, for some characters in some positions. Otherwise it implies an increment."
[rhs breakpoint increment]
(if (>= rhs breakpoint)
(- rhs increment)
(+ rhs increment)))
(defn read-roman
"Read a roman numeral and return its value."
[[character & remainder]]
(if (nil? character) 0
(let [rhs (read-roman remainder)]
(cond
(= character \I) (incordec rhs 5 1)
(= character \V) (+ 5 rhs)
(= character \X) (incordec rhs 50 10)
(= character \L) (+ 50 rhs)
(= character \C) (incordec rhs 500 100)
(= character \D) (+ 500 rhs)
(= character \M) (+ 1000 rhs)
:true (throw
(NumberFormatException.
(format "Did not recognise the character '%c'", character)))))))
(defn write-roman
"Write a roman numeral. This is (obviously) not an optimal solution,
but will do for now."
[n]
(cond
(> 1 n) ""
(>= n 1000) (str "M" (write-roman (- n 1000)))
(>= n 900) (str "CM" (write-roman (- n 900)))
(>= n 500) (str "D" (write-roman (- n 500)))
(>= n 400) (str "CD" (write-roman (- n 400)))
(>= n 100) (str "C" (write-roman (- n 100)))
(>= n 90) (str "XC" (write-roman (- n 90)))
(>= n 50) (str "L" (write-roman (- n 50)))
(>= n 40) (str "XL" (write-roman (- n 40)))
(>= n 10) (str "X" (write-roman (- n 10)))
(= n 9) "IX"
(>= n 6) (str "VI" (write-roman (- n 6))) ;; workaround for odd behaviour printing VIII
(>= n 5) (str "V" (write-roman (- n 5)))
(= n 4) "IV"
(>= n 1) (str "I" (write-roman (- n 1)))))
(defn r-op
"Apply this operator to these arguments, expected to be roman numerals and
return the result as a roman numeral"
[operator & args]
(write-roman
(int
(apply operator
(map read-roman args)))))
(defn r-op2
"Apply this operator to these two arguments, expected to be roman numerals"
[operator arg1 arg2]
(r-op operator arg1 arg2))
(defn r-plus
"Add arbitrarily many roman numerals and return a roman numeral"
[& args]
(apply r-op (cons + args)))
(defn r-minus
"Subtract a roman numeral from a roman numeral and return a roman numeral"
[arg1 arg2]
(apply r-op (list - arg1 arg2)))
(defn r-multiply
"Multiply arbitrarily many roman numerals and return a roman numeral"
[& args]
(apply r-op (cons * args)))
(defn r-divide
"Divide a roman numeral by a roman numeral and return a roman numeral"
[arg1 arg2]
(apply r-op (list / arg1 arg2)))

View file

@ -0,0 +1,113 @@
(ns antonine.calculator
(:require [clojure.math :refer [floor]]
[instaparse.core :refer [get-failure]])
(:import [java.lang NumberFormatException]))
(defn incordec
"In reading roman numerals, a lower value character preceding a higher value may imply a decrement
to the higher value, for some characters in some positions. Otherwise it implies an increment."
[rhs breakpoint increment]
(if (>= rhs breakpoint)
(- rhs increment)
(+ rhs increment)))
(defn- read-roman-char [c accumulator]
(cond
(= c \I) (incordec accumulator 5 1)
(= c \V) (+ 5 accumulator)
(= c \X) (incordec accumulator 50 10)
(= c \L) (+ 50 accumulator)
(= c \C) (incordec accumulator 500 100)
(= c \D) (+ 500 accumulator)
(= c \M) (+ 1000 accumulator)
:else (throw
(NumberFormatException.
(format "Did not recognise the character '%c'", c)))))
(defn read-roman
"Read this `input`, interpreting it as a Roman numeral, and return the
integer value."
[input]
(loop [c (last input) r (butlast input) accumulator 0]
(let [v (read-roman-char c accumulator)]
(if (empty? r) v
(recur (last r) (butlast r) v)))))
(defn write-roman
"Write, as a roman numeral, the number `n`."
[n]
(cond
(> 1 n) "" ;; Romans don't bother with pesky fractions. Pi is 3!
(>= n 1000) (str "M" (write-roman (- n 1000)))
(>= n 900) (str "CM" (write-roman (- n 900)))
(>= n 500) (str "D" (write-roman (- n 500)))
(>= n 400) (str "CD" (write-roman (- n 400)))
(>= n 100) (str "C" (write-roman (- n 100)))
(>= n 90) (str "XC" (write-roman (- n 90)))
(>= n 50) (str "L" (write-roman (- n 50)))
(>= n 40) (str "XL" (write-roman (- n 40)))
(>= n 10) (str "X" (write-roman (- n 10)))
(= n 9) "IX"
(>= n 6) (str "VI" (write-roman (- n 6))) ;; workaround for odd behaviour printing VIII
(>= n 5) (str "V" (write-roman (- n 5)))
(= n 4) "IV"
(>= n 1) (str "I" (write-roman (- n 1)))))
(defn r-op
"Apply this operator to these arguments, expected to be roman numerals and
return the result as a roman numeral"
[operator & args]
(write-roman
(int
(apply operator
(map read-roman args)))))
(defn r-op2
"Apply this operator to these two arguments, expected to be roman numerals"
[operator arg1 arg2]
(r-op operator arg1 arg2))
(defn r-plus
"Add arbitrarily many roman numerals and return a roman numeral"
[& args]
(apply r-op (cons + args)))
(defn r-minus
"Subtract a roman numeral from a roman numeral and return a roman numeral"
[arg1 arg2]
(apply r-op (list - arg1 arg2)))
(defn r-multiply
"Multiply arbitrarily many roman numerals and return a roman numeral"
[& args]
(apply r-op (cons * args)))
(defn r-divide
"Divide a roman numeral by a roman numeral and return a roman numeral"
[arg1 arg2]
(apply r-op (list / arg1 arg2)))
(defn calculate
[parse-tree]
(let [failure-text (:text (get-failure parse-tree))]
(if (= :EXPRESSION (first parse-tree))
(case (count parse-tree)
2 (read-roman (second parse-tree))
4 (let [lhs (read-roman (second parse-tree))
op (case (first (nth parse-tree 2))
:ADD +
:MULTIPLY *
:SUBTRACT -
:DIVIDE /)
rhs (calculate (nth parse-tree 3))]
(int (floor (apply op (list lhs rhs)))))
;;else
(throw
(ex-info
(format "Unexpected parse tree '%s'" failure-text)
{:problem parse-tree})))
(throw
(ex-info
(format "Unexpected expression: '%s'" failure-text)
{:problem parse-tree})))))

View file

@ -0,0 +1,55 @@
(ns antonine.char-reader
"Provide sensible line editing and history recall."
(:import [org.jline.reader.impl.completer StringsCompleter]
[org.jline.reader.impl DefaultParser DefaultParser$Bracket]
[org.jline.reader LineReaderBuilder]
[org.jline.terminal TerminalBuilder]
[org.jline.widget AutopairWidgets AutosuggestionWidgets]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Copyright (C) 2022-2024 Simon Brooke
;;;
;;; 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 2
;;; 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, write to the Free Software
;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Adapted (simplified) from the Beowulf line reader, this allows history and
;; line editing but really nothing more sophisticated. It ought not to allow
;; brackets, since Antonine doesn't allow these, but presently it does.
(def get-reader
"Return a reader, first constructing it if necessary.
**NOTE THAT** this is not settled API. The existence and call signature of
this function is not guaranteed in future versions."
(memoize (fn []
(let [term (.build (.system (TerminalBuilder/builder) true))]
(-> (LineReaderBuilder/builder)
(.terminal term)
(.build))))))
(defn read-chars
"A drop-in replacement for `clojure.core/read-line`, except that line editing
and history should be enabled."
[prompt]
(let [eddie (get-reader)]
(loop [s (.readLine eddie (str prompt " "))]
(if (and (= (count (re-seq #"\(" s))
(count (re-seq #"\)" s)))
(= (count (re-seq #"\[]" s))
(count (re-seq #"\]" s))))
s
(recur (str s " " (.readLine eddie ":: ")))))))

94
src/antonine/core.clj Normal file
View file

@ -0,0 +1,94 @@
(ns antonine.core
(:require [antonine.calculator :refer [calculate write-roman]]
[antonine.char-reader :refer [read-chars]]
[clojure.java.io :refer [resource]]
[clojure.pprint :refer [pprint]]
[clojure.string :as s :refer [trim upper-case]]
[clojure.tools.cli :refer [parse-opts]]
[instaparse.core :refer [parser]])
(:gen-class))
(defn- romanise [arg]
(s/replace
(s/replace
(upper-case arg)
"J" "I")
"U" "V"))
(def cli-options
[["-b" "--banner BANNER" "The welcome banner to display"
:default (romanise "Antonius ornare | Simon rivulus hoc fecit | MMXXIV")]
["-p" "--prompt PROMPT" "The prompt to display"
:default "COMPVTARE | "]
["-s" "--stop-word WORD" "The stop word to use at the end of a session"
:default "FINIS"]
;; A non-idempotent option (:default is applied first)
["-v" "--verbosity LEVEL" "Verbosity level"
:default 0
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 6) "Must be a number between 0 and 5"]]
;; A boolean option defaulting to nil
["-h" "--help"]])
(def grammar (parser (resource "grammar.bnf")))
(defn repl
"Read/eval/print loop, using these command line `options`."
[options]
(let [prompt (:prompt options)
stop-word (:stop-word options)
vrb (:verbosity options)]
(try (loop []
(flush)
(try
(if-let [input (trim (upper-case (read-chars prompt)))]
(if (or (empty? input) (= input stop-word))
(throw
(ex-info
(format "\nVALE %s" (romanise (System/getProperty "user.name")))
{:cause :quit}))
(let [tree (grammar input)
v (calculate tree)]
(when (> vrb 1)
(println (format "(Parse tree: %s)" tree)))
(when (> vrb 0) (println (format "(Arabic: %d)" v)))
(println (write-roman v))))
(println))
(catch
Exception
e
(let [data (ex-data e)]
(println (.getMessage e))
(when
data
(case (:cause data)
:parse-failure (println (:failure data))
:strict nil ;; the message, which has already been printed, is enough.
:quit (throw e)
;; default
(pprint data))))))
(recur))
(catch Throwable i
(if (= :quit (:cause (ex-data i)))
nil
(throw i))))))
(defn -main [& args]
(let [{:keys [options arguments errors summary]} (parse-opts args cli-options)]
(when errors
(doall (map println errors))
(println summary)
(System/exit 1))
(when (and (empty? arguments) (:banner options))
(println (:banner options))
(println))
(when (>= (:verbosity options) 5)
(pprint (parse-opts args cli-options))
(println))
(when (:help options)
(println summary)
(System/exit 0))
(if (empty? arguments)
(repl options)
(println (write-roman (calculate (grammar (trim (s/join " " arguments)))))))))