From 4d3c36959c28f497c8f114cddad4eebf44830253 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Sat, 8 Jun 2024 14:49:55 +0100 Subject: [PATCH] Added a core namespace with a -main method; upversioned. Also, Interactive read-eval-print loop, debugging, and other minor enhancements. --- .gitignore | 31 ++++++++++ README.md | 57 +++++++++++++++++- doc/intro.md | 77 ++++++++++++++++++++++++ project.clj | 13 +++- resources/grammar.bnf | 8 +++ src/antonine/calculator.clj | 86 -------------------------- src/antonine/calculator.cljc | 113 +++++++++++++++++++++++++++++++++++ src/antonine/char_reader.clj | 55 +++++++++++++++++ src/antonine/core.clj | 94 +++++++++++++++++++++++++++++ 9 files changed, 444 insertions(+), 90 deletions(-) create mode 100644 .gitignore create mode 100644 doc/intro.md create mode 100644 resources/grammar.bnf delete mode 100644 src/antonine/calculator.clj create mode 100644 src/antonine/calculator.cljc create mode 100644 src/antonine/char_reader.clj create mode 100644 src/antonine/core.clj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da59ff0 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index c8dc795..765e483 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/doc/intro.md b/doc/intro.md new file mode 100644 index 0000000..df373c2 --- /dev/null +++ b/doc/intro.md @@ -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. diff --git a/project.clj b/project.clj index a427df7..f258f07 100644 --- a/project.clj +++ b/project.clj @@ -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}} + ) diff --git a/resources/grammar.bnf b/resources/grammar.bnf new file mode 100644 index 0000000..d9ce30c --- /dev/null +++ b/resources/grammar.bnf @@ -0,0 +1,8 @@ +EXPRESSION := NUMBER | NUMBER OPERATOR EXPRESSION + := #'[IVXLCDM]+' + := ADD | SUBTRACT | MULTIPLY | DIVIDE +ADD := <'+'> +SUBTRACT := <'-'> +MULTIPLY := <'x'> | <'*'> +DIVIDE := <'/'> +SPACE := #'(?U)\s+' \ No newline at end of file diff --git a/src/antonine/calculator.clj b/src/antonine/calculator.clj deleted file mode 100644 index e2bc107..0000000 --- a/src/antonine/calculator.clj +++ /dev/null @@ -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))) - - - - diff --git a/src/antonine/calculator.cljc b/src/antonine/calculator.cljc new file mode 100644 index 0000000..e224100 --- /dev/null +++ b/src/antonine/calculator.cljc @@ -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}))))) + diff --git a/src/antonine/char_reader.clj b/src/antonine/char_reader.clj new file mode 100644 index 0000000..a62522d --- /dev/null +++ b/src/antonine/char_reader.clj @@ -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 ":: "))))))) diff --git a/src/antonine/core.clj b/src/antonine/core.clj new file mode 100644 index 0000000..94a99aa --- /dev/null +++ b/src/antonine/core.clj @@ -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))))))))) \ No newline at end of file