Feature enhancement: float options
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.
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.
- Create a new class, which inherits from
CLINGON:OPTION - Implement
CLINGON:INITIALIZE-OPTION, so that the option can be initialized from env vars, etc. - Implement
CLINGON:DERIVE-OPTION-VALUE - 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! :)
Thanks for that. I will push it onto my todo list, and hope to get to it pretty soon...