There is a requirement to implement a decision table as below:
MemberType Amount => Discount
"Guest" > 2000 => 3%
"Silver" any => 5%
"Silver" > 1000 => 10%
"Gold" any => 15%
"Gold" > 500 => 20%
I would imagine, if properly implemented in Clojure, we can define a rule table as below:
(defrule calc-discount
[member-type amount]
"Guest" (greater-than 2000) => 0.03
"Silver" (anything) => 0.05
"Silver" (greater-than 1000) => 0.1
"Gold" (anything) => 0.15
"Gold" (greater-than 500) => 0.2
)
Of course, there should be a better way in writing/defining such a rule set. Yet the key thing I think is how to define "defrule" to make this happen?
Use core.match! It's a Clojure library for pattern matching.
Your example would turn out a little something like this....
(let [member-type "Gold"
amount 600]
(match [member-type amount]
["Guest" (_ :guard #(> % 2000))] 0.03
["Silver" (_ :guard #(> % 1000))] 0.1
["Silver" _] 0.05
["Gold" (_ :guard #(> % 500))] 0.2
["Gold" _] 0.15
:else 0))
; => 0.2
For this example, you can express the business logic rather concisely with condp.
(defn discount
[member-type amount]
(condp (fn [[type tier] _] (and (= member-type type) (> amount tier))) nil
["Guest" 2000] 0.03
["Silver" 1000] 0.10
["Silver" 0] 0.05
["Gold" 500] 0.20
["Gold" 0] 0.15
0.00))
(discount "Gold" 600) ;=> 0.2
If you are looking to implement the syntax as in your example, you'll need to write a macro. A very rough example:
(defmacro defrule [name fields & clauses]
(let [exp (fn [f c] (if (list? c) (list* (first c) f (rest c)) (list `= c f)))]
`(defn ~name ~fields
(cond
~#(for [clause (partition-all (+ 2 (count fields)) clauses)
form [(cons `and (map exp fields clause)) (last clause)]]
form)))))
(def any (constantly true))
(defrule calc-discount
[member-type amount]
"Guest" (> 2000) => 0.03
"Silver" (> 1000) => 0.10
"Silver" (any) => 0.05
"Gold" (> 500) => 0.20
"Gold" (any) => 0.15)
(calc-discount "Silver" 1234) ;=> 0.10
It seems like what you want is a relatable, visual representation in code of various combinations of conditions and their corresponding outcomes, in tabular format.
Check out the cond-table macro, which expands your tabular form into a regular cond form:
https://github.com/semperos/rankle/blob/master/src/com/semperos/rankle/util.clj
Using this approach, your example could be coded like so:
(defn discount-for [memtype amount]
(let [spends? (fn [catgy pred] (and (= memtype catgy)
(pred amount)))
>n (fn [n] #(> % n))
any #(< 0 % 501)]
(util/cond-table
:| spends? (>n 2000) (>n 1000) (>n 500) any
:| "Guest" 0.03 0 0 0
:| "Silver" 0.10 0.10 0.05 0.05
:| "Gold" 0.20 0.20 0.20 0.15)))
(discount-for "Guest" 3000)
;;=> 0.03
(discount-for "Gold" 25.95)
;;=> 0.15
Related
The result of frequencies is wrong when used for sequencies containing NaNs, for example:
=> (frequencies [Double/NaN Double/NaN])
{NaN 1, NaN 1}
instead of expected {NaN 2}.
Furthermore, the running time deteriorates from expected/average O(n) to worst-case O(n^2), e.g.
=> (def v3 (vec (repeatedly 1e3 #(Double/NaN))))
=> (def r (time (frequencies v3)))
"Elapsed time: 36.081751 msecs"
...
=> (def v3 (vec (repeatedly 1e3 #(Double/NaN))))
=> (def r (time (frequencies v3)))
"Elapsed time: 3358.490101 msecs"
...
i.e. 10 times so many elements need 100 times higher running time.
How can frequencies be calculated with (expected/average) O(n) running time, when NaNs are present in the sequence?
As side note:
=> (frequencies (repeat 1e3 Double/NaN))
{NaN 1000}
yields the expected result, probably because all elements in the sequence are references of the same object.
NaN is pretty weird in many programming languages, partly because the IEEE 754 standard for floating point numbers defines that NaN should not equal anything, not even itself. It is the "not even itself" part that leads to most of the weird behavior you are seeing. More here, if you are curious: https://github.com/jafingerhut/batman
The sample function below may be adaptable to your needs. It uses :nan-kw in the returned map to indicate how many NaNs were found. If you replace :nan-kw with ##NaN, then the returned map has the disadvantage that you cannot find the count with (get frequency-ret-value ##NaN), because of the weirdness of ##NaN.
(defn frequencies-maybe-nans [s]
(let [separate-nans (group-by #(and (double? %) (Double/isNaN %)) s)
num-nans (count (separate-nans true))]
(merge (frequencies (separate-nans false))
(when-not (zero? num-nans)
{:nan-kw num-nans}))))
(def freqs (frequencies-maybe-nans [1 2 ##NaN 5 5]))
freqs
(get freqs 2)
(get freqs :nan-kw)
Some background on NaN values on the JVM: https://www.baeldung.com/java-not-a-number
You can solve this by encoding the NaN values temporarily while computing the frequencies:
(ns tst.demo.core
(:use tupelo.core
tupelo.test))
(defn is-NaN? [x] (.isNaN x))
(defn nan-encode
[arg]
(if (is-NaN? arg)
::nan
arg))
(defn nan-decode
[arg]
(if (= ::nan arg)
Double/NaN
arg))
(defn freq-nan
[coll]
(it-> coll
(mapv nan-encode it)
(frequencies it)
(map-keys it nan-decode)))
(dotest
(let [x [1.0 2.0 2.0 Double/NaN Double/NaN Double/NaN]]
(is= (spyx (freq-nan x)) {1.0 1,
2.0 2,
##NaN 3})))
with result:
-------------------------------
Clojure 1.10.1 Java 13
-------------------------------
Testing tst.demo.core
(freq-nan x) => {1.0 1, 2.0 2, ##NaN 3}
FAIL in (dotest-line-25) (core.clj:27)
expected: (clojure.core/= (spyx (freq-nan x)) {1.0 1, 2.0 2, ##NaN 3})
actual: (not (clojure.core/= {1.0 1, 2.0 2, ##NaN 3} {1.0 1, 2.0 2, ##NaN 3}))
Note that even though it calculates & prints the correct result, the unit test still fails since NaN is never equal to anything, even itself. If you want the unit test to pass, you need to leave in the placeholder ::nan like:
(defn freq-nan
[coll]
(it-> coll
(mapv nan-encode it)
(frequencies it)
))
(dotest
(let [x [1.0 2.0 2.0 Double/NaN Double/NaN Double/NaN]]
(is= (spyx (freq-nan x)) {1.0 1,
2.0 2,
::nan 3})))
(defn to-percentage [wins total]
(if (= wins 0) 0
(* (/ wins total) 100)))
(defn calc-winrate [matches]
(let [data (r/atom [])]
(loop [wins 0
total 0]
(if (= total (count matches))
#data
(recur (if (= (get (nth matches total) :result) 1)
(inc wins))
(do
(swap! data conj (to-percentage wins total))
(inc total)))))))
(calc-winrate [{:result 0} {:result 1} {:result 0} {:result 1} {:result 1}])
I got the following code, calc-winrate on the last line returns [0 0 50 0 25]. I'm trying to make it return [0 50 33.33333333333333 50 60].
Am I doing the increment for wins wrong? When I print the value of wins for each iteration I get
0
nil
1
nil
1
so I'm guessing I somehow reset or nil wins somehow?
Also, could this whole loop be replaced with map/map-indexed or something? It feels like map would be perfect to use but I need to keep the previous iteration wins/total in mind for each iteration.
Thanks!
Here's a lazy solution using reductions to get a sequence of running win totals, and transducers to 1) join the round numbers with the running totals 2) divide the pairs 3) convert fractions to percentages:
(defn calc-win-rate [results]
(->> results
(map :result)
(reductions +)
(sequence
(comp
(map-indexed (fn [round win-total] [win-total (inc round)]))
(map (partial apply /))
(map #(* 100 %))
(map float)))))
(calc-win-rate [{:result 0} {:result 1} {:result 0} {:result 1} {:result 1}])
=> (0.0 50.0 33.333332 50.0 60.0)
You can calculate the running win rates as follows:
(defn calc-winrate [matches]
(map
(comp float #(* 100 %) /)
(reductions + (map :result matches))
(rest (range))))
For example,
=> (calc-winrate [{:result 0} {:result 1} {:result 0} {:result 1} {:result 1}])
(0.0 50.0 33.333332 50.0 60.0)
The map operates on two sequences:
(reductions + (map :result matches)) - the running total of wins;
(rest (range)))) - (1 2 3 ... ), the corresponding number of matches.
The mapping function, (comp float #(* 100 %) /),
divides the corresponding elements of the sequences,
multiplies it by 100, and
turns it into floating point.
Here's a solution with reduce:
(defn calc-winrate [matches]
(let [total-matches (count matches)]
(->> matches
(map :result)
(reduce (fn [{:keys [wins matches percentage] :as result} match]
(let [wins (+ wins match)
matches (inc matches)]
{:wins wins
:matches matches
:percentage (conj percentage (to-percentage wins matches))}))
{:wins 0
:matches 0
:percentage []}))))
So the thing here is to maintain (and update) the state of the calculation thus far.
We do that in the map that's
{:wins 0
:matches 0
:percentage []}
Wins will contain the wins so far, matches are the number of matches we've analysed, and percentage is the percentage for so far.
(if (= (get (nth matches total) :result) 1)
(inc wins))
your if shall be written as follows:
(if (= (get (nth matches total) :result) 1)
(inc wins)
wins ; missing here , other wise it will return a nil, binding to wins in the loop
)
if you go with a reductions ,
(defn calc-winrate2 [ x y ]
(let [ {total :total r :wins } x
{re :result } y]
(if (pos? re )
{:total (inc total) :wins (inc r)}
{:total (inc total) :wins r}
)
)
)
(reductions calc-winrate2 {:total 0 :wins 0} [ {:result 0} {:result 1} {:result 0} {:result 1} {:result 1}])
(defn multiply-xf
[]
(fn [xf]
(let [product (volatile! 1)]
(fn
([] (xf))
([result]
(xf result #product)
(xf result))
([result input]
(let [new-product (* input #product)]
(vreset! product new-product)
(if (zero? new-product)
(do
(println "reduced")
(reduced ...)) <----- ???
result)))))))
This is a simple transducer which multiples numbers. I am wondering what would be the reduced value to allow early termination?
I've tried (transient []) but that means the transducer only works with vectors.
I'm assuming you want this transducer to produce a running product sequence and terminate early if the product reaches zero. Although in the example the reducing function xf is never called in the 2-arity step function, and it's called twice in the completion arity.
(defn multiply-xf
[]
(fn [rf]
(let [product (volatile! 1)]
(fn
([] (rf))
([result] (rf result))
([result input]
(let [new-product (vswap! product * input)]
(if (zero? new-product)
(reduced result)
(rf result new-product))))))))
Notice for early termination, we don't care what result is. That's the responsibility of the reducing function rf a.k.a xf in your example. I also consolidated vreset!/#product with vswap!.
(sequence (multiply-xf) [2 2 2 2 2])
=> (2 4 8 16 32)
It will terminate if the running product reaches zero:
(sequence (multiply-xf) [2 2 0 2 2])
=> (2 4)
We can use transduce to sum the output. Here the reducing function is +, but your transducer doesn't need to know anything about that:
(transduce (multiply-xf) + [2 2 2 2])
=> 30
I've tried (transient []) but that means the transducer only works with vectors.
This transducer also doesn't need to concern itself the type of sequence/collection it's given.
(eduction (multiply-xf) (range 1 10))
=> (1 2 6 24 120 720 5040 40320 362880)
(sequence (multiply-xf) '(2.0 2.0 0.5 2 1/2 2 0.5))
=> (2.0 4.0 2.0 4.0 2.0 4.0 2.0)
(into #{} (multiply-xf) [2.0 2.0 0.5 2 1/2 2 0.5])
=> #{2.0 4.0}
This can be done without transducers as well:
(take-while (complement zero?) (reductions * [2 2 0 2 2]))
=> (2 4)
Say we have a function get-ints with one positional argument, the number of ints the caller wants, and two named arguments :max and :min like:
; Ignore that the implementation of the function is incorrect.
(defn get-ints [nr & {:keys [max min] :or {max 10 min 0}}]
(take nr (repeatedly #(int (+ (* (rand) (- max min -1)) min)))))
(get-ints 5) ; => (8 4 10 5 5)
(get-ints 5 :max 100) ; => (78 43 32 66 6)
(get-ints 5 :min 5) ; => (10 5 9 9 9)
(get-ints 5 :min 5 :max 6) ; => (5 5 6 6 5)
How does one write a Plumatic Schema for the argument list of get-ints, a list of one, three or five items where the first one is always a number and the following items are always pairs of a keyword and an associated value.
With Clojure Spec I'd express this as:
(require '[clojure.spec :as spec])
(spec/cat :nr pos-int? :args (spec/keys* :opt-un [::min ::max]))
Along with the separate definitions of valid values held by ::min and ::max.
I think this is a case when it is easier to write the specific code you need rather than trying to force-fit a solution using Plumatic Schema or some other tool that is not designed for this use-case. Keep in mind that Plumatic Schema & other tools (like the built-in Clojure pre- & post-conditions) are just a shorthand way of throwing an Exception when some condition is violated. If none of these DSL's are suitable, you always have the general-purpose language to fall back on.
A similar situation to yours can be found in the Tupelo library for the rel= function. It is designed to perform a test for "relative equality" between two numbers. It works like so:
(is (rel= 123450000 123456789 :digits 4 )) ; .12345 * 10^9
(is (not (rel= 123450000 123456789 :digits 6 )))
(is (rel= 0.123450000 0.123456789 :digits 4 )) ; .12345 * 1
(is (not (rel= 0.123450000 0.123456789 :digits 6 )))
(is (rel= 1 1.001 :tol 0.01 )) ; :tol value is absolute error
(is (not (rel= 1 1.001 :tol 0.0001 )))
While nearly all other functions in the Tupelo library make heavy use of Plumatic Schema, this one does it "manually":
(defn rel=
"Returns true if 2 double-precision numbers are relatively equal, else false. Relative equality
is specified as either (1) the N most significant digits are equal, or (2) the absolute
difference is less than a tolerance value. Input values are coerced to double before comparison.
Example:
(rel= 123450000 123456789 :digits 4 ) ; true
(rel= 1 1.001 :tol 0.01) ; true
"
[val1 val2 & {:as opts}]
{:pre [(number? val1) (number? val2)]
:post [(contains? #{true false} %)]}
(let [{:keys [digits tol]} opts]
(when-not (or digits tol)
(throw (IllegalArgumentException.
(str "Must specify either :digits or :tol" \newline
"opts: " opts))))
(when tol
(when-not (number? tol)
(throw (IllegalArgumentException.
(str ":tol must be a number" \newline
"opts: " opts))))
(when-not (pos? tol)
(throw (IllegalArgumentException.
(str ":tol must be positive" \newline
"opts: " opts)))))
(when digits
(when-not (integer? digits)
(throw (IllegalArgumentException.
(str ":digits must be an integer" \newline
"opts: " opts))))
(when-not (pos? digits)
(throw (IllegalArgumentException.
(str ":digits must positive" \newline
"opts: " opts)))))
; At this point, there were no invalid args and at least one of
; either :tol and/or :digits was specified. So, return the answer.
(let [val1 (double val1)
val2 (double val2)
delta-abs (Math/abs (- val1 val2))
or-result (truthy?
(or (zero? delta-abs)
(and tol
(let [tol-result (< delta-abs tol)]
tol-result))
(and digits
(let [abs1 (Math/abs val1)
abs2 (Math/abs val2)
max-abs (Math/max abs1 abs2)
delta-rel-abs (/ delta-abs max-abs)
rel-tol (Math/pow 10 (- digits))
dig-result (< delta-rel-abs rel-tol)]
dig-result))))
]
or-result)))
Based on the answer I got from the Plumatic mailing list [0] [1] I sat down and wrote my own conformer outside of the schema language itself:
(defn key-val-seq?
([kv-seq]
(and (even? (count kv-seq))
(every? keyword? (take-nth 2 kv-seq))))
([kv-seq validation-map]
(and (key-val-seq? kv-seq)
(every? nil? (for [[k v] (partition 2 kv-seq)]
(if-let [schema (get validation-map k)]
(schema/check schema v)
:schema/invalid))))))
(def get-int-args
(schema/constrained
[schema/Any]
#(and (integer? (first %))
(key-val-seq? (rest %) {:max schema/Int :min schema/Int}))))
(schema/validate get-int-args '()) ; Exception: Value does not match schema...
(schema/validate get-int-args '(5)) ; => (5)
(schema/validate get-int-args [5 :max 10]) ; => [5 :max 10]
(schema/validate get-int-args [5 :max 10 :min 1]); => [5 :max 10 :min 1]
(schema/validate get-int-args [5 :max 10 :b 1]) ; Exception: Value does not match schema...
If I've got a sorted-set of floats, how can I find the smallest difference between any 2 values in that sorted set?
For example, if the sorted set contains
#{1.0 1.1 1.3 1.45 1.7 1.71}
then the result I'm after would be 0.01, as the difference between 1.71 and 1.7 is the smallest difference between any 2 values in that sorted set.
EDIT
As Alan pointed out to me, the problem stated this was a sorted set, so we could do this much simpler:
(def s (sorted-set 1.0 1.1 1.3 1.45 1.7 1.71))
(reduce min (map - (rest s) s)))
=> 0.01
Original Answer
Assuming the set is unordered, although ordering it might be better.
Given
(def s #{1.0 1.1 1.3 1.45 1.7 1.71})
We could get relevant pairs, as in, for every number in the list, pair it with all the numbers to the right of it:
(def pairs
(loop [r [] s (into [] s)]
(if-let [[f & v] s]
(recur (concat r (for [i v] [f i]))
v)
r)))
=> ([1.0 1.45] [1.0 1.7] [1.0 1.3] [1.0 1.1] [1.0 1.71] [1.45 1.7] [1.45 1.3]
[1.45 1.1] [1.45 1.71] [1.7 1.3] [1.7 1.1] [1.7 1.71] [1.3 1.1] [1.3 1.71]
[1.1 1.71])
Now, we will want to look at the absolute values of the differences between every pair:
(defn abs [x] (Math/abs x))
Put it all together, and get the minimum value:
(reduce min (map (comp abs (partial apply -)) pairs))
Which will give us the desired output, 0.01
That last line could be more explicitly written as
(reduce min
(map (fn[[a b]]
(abs (- a b)))
pairs))
I think using the Clojure built-in function partition is the simplest way:
(ns clj.core
(:require [tupelo.core :as t] ))
(t/refer-tupelo)
(def vals [1.0 1.1 1.3 1.45 1.7 1.71])
(spyx vals)
(def pairs (partition 2 1 vals))
(spyx pairs)
(def deltas (mapv #(apply - (reverse %)) pairs))
(spyx deltas)
(println "result=" (apply
vals => [1.0 1.1 1.3 1.45 1.7 1.71]
pairs => ((1.0 1.1) (1.1 1.3) (1.3 1.45) (1.45 1.7) (1.7 1.71))
deltas => [0.10000000000000009 0.19999999999999996 0.1499999999999999 0.25 0.010000000000000009]
result= 0.010000000000000009