dnaeon / clingon

Command-line options parser system for Common Lisp
Other
122 stars 7 forks source link

Feature enhancement: float options #9

Open rpgoldman opened 1 year ago

rpgoldman commented 1 year ago

For scientific / numerical options, it would be very helpful to have options that accept floating point (decimal number) values.

For now, I hack around this by using :string and then parsing it myself. I wasn't confident I could add a new option type myself.

dnaeon commented 1 year ago

Hey @rpgoldman ,

Here's how you can create a new option (feel free to submit a PR for it):

The code, which implements a new float option looks like this. The steps are pretty much these.

  1. Create a new class, which inherits from CLINGON:OPTION
  2. Implement CLINGON:INITIALIZE-OPTION, so that the option can be initialized from env vars, etc.
  3. Implement CLINGON:DERIVE-OPTION-VALUE
  4. Implement CLINGON:MAKE-OPTION

Here's what the code looks like.

;; Load systems
(ql:quickload :clingon)
(ql:quickload :parse-float)

;; Our sample package
(defpackage :clingon.extensions/option-float
  (:use :cl)
  (:import-from :parse-float)
  (:import-from
   :clingon
   :option
   :make-option
   :option-derive-error
   :initialize-option
   :option-value
   :derive-option-value)
  (:export
   :option-float
   :option-float-radix
   :parse-float-or-lose))
(in-package :clingon.extensions/option-float)

(defun parse-float-or-lose (value &key (radix 10))
  (when (floatp value)
    (return-from parse-float-or-lose value))
  (let ((f (parse-float:parse-float value :radix radix :junk-allowed t)))
    (unless f
      (error 'option-derive-error :reason (format nil "Cannot parse ~A as float" value)))
    f))

(defclass option-float (option)
  ((radix
    :initarg :radix
    :initform 10
    :reader option-float-radix))
   (:default-initargs
    :parameter "FLOAT")
   (:documentation "An option class to represent float numbers"))

(defmethod make-option ((kind (eql :float)) &rest rest)
  (apply #'make-instance 'option-float rest))

(defmethod initialize-option ((option option-float) &key)
  "Initializes our option"
  ;; Make sure to invoke our parent initialization method first, so
  ;; various things like setting up initial value from environment
  ;; variables can still be applied.
  (call-next-method)

  ;; If we don't have any value set, there's nothing else to
  ;; initialize further here.
  (unless (option-value option)
    (return-from initialize-option))

  ;; If we get to this point, that means we've got some initial value,
  ;; which is either set as a default, or via environment
  ;; variables. Next thing we need to do is make sure we've got a good
  ;; initial value, so let's derive a value from it.
  (let ((current (option-value option)))
    (setf (option-value option)
          (derive-option-value option current))))

(defmethod derive-option-value ((option option-float) value &key)
  (let ((radix (option-float-radix option)))
    (parse-float-or-lose value :radix radix)))

And testing it out on the REPL.

CL-USER> (defparameter *opt*
       (clingon:make-option :float :short-name #\f :key :my-float :description "some float"))
*OPT*
CL-USER> (clingon:derive-option-value *opt* "10")
10.0
CL-USER> (clingon:derive-option-value *opt* "10e-2")
0.1 (10.0%)
CL-USER> (clingon:derive-option-value *opt* "1.2e-3")
0.0012 (0.120000005%)

You can also check #2 for additional examples about new options. Feel free to add anything else to this code and submit it as a PR! :)

rpgoldman commented 1 year ago

Thanks for that. I will push it onto my todo list, and hope to get to it pretty soon...