Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oposite of try-all: try-all-failed and similar #36

Open
ieugen opened this issue Apr 9, 2024 · 6 comments
Open

Oposite of try-all: try-all-failed and similar #36

ieugen opened this issue Apr 9, 2024 · 6 comments

Comments

@ieugen
Copy link

ieugen commented Apr 9, 2024

I think try-all-failed and similar might also be useful.

I am trying to parse some input from user (a time Duration) and trying to be forgiving.
In my case I need to try out all options and stop when I get a non-failure.

So my algorithm is :

  • try to parse user input
  • if failed try to parse user input with "PT" pre-pended
  • if failed try to parse user input with "P" pre-pended
  • give up with parsing
  • or if one did not fail, return that value

I think code would look like this:
(beginner with Failjure, code might not be correct)

(try-all-failed [duration (try-parse s)
                         duration (try-parse (str "PT" s))
                         duration (try-parse (str "P" s))]
duration
(f/fail "Unable to parse duration"))

Some context
https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-

 Examples:

    "PT20.345S" -- parses as "20.345 seconds"
    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
    "P-6H3M"    -- parses as "-6 hours and +3 minutes"
    "-P6H3M"    -- parses as "-6 hours and -3 minutes"
    "-P-6H+3M"  -- parses as "+6 hours and -3 minutes"
@ieugen
Copy link
Author

ieugen commented Apr 9, 2024

Currently I do things like this:

(ns user
  (:require
            [clojure.string :as str]
            [failjure.core :as f])
  (:import (java.time Duration)
           (java.time.format DateTimeParseException)))


(defn try-parse-duration
  "Try to parse a duration.
   Return duration on success.
   Return a Failure on exception."
  [duration]
  (f/try*
   (Duration/parse duration)))

(defn parse-duration
  "Parse a duration from string.
   Return a ^java.lang.Duration on success.
   Retun nil when string is blank or empty.

   Throw exception on parse failure."
  ;;TODO: Attempt to accept go time.Duration string format.
  [duration-str]
  (f/when-let-failed?
   [_duration (try-parse-duration duration-str)]
   (f/when-let-failed?
    [_duration (try-parse-duration (str "PT" duration-str))]
    (f/when-let-failed?
     [_duration (try-parse-duration (str "P" duration-str))]
     (f/fail "Failed to parse duration %s" duration-str)))))

^:rct/test
(comment

  (parse-duration nil)
  ;; => #failjure.core.Failure{:message "Failed to parse duration null"}

  (parse-duration "")
  ;; => #failjure.core.Failure{:message "Failed to parse duration "}

  (parse-duration "P")
  ;; => #failjure.core.Failure{:message "Failed to parse duration P"}

  (parse-duration "1")
  ;; => #failjure.core.Failure{:message "Failed to parse duration 1"}

  (parse-duration "1s")
  ;; => #object[java.time.Duration 0x586eb169 "PT1S"]

  (parse-duration "1d")
  ;; => #object[java.time.Duration 0x7f2e8c60 "PT24H"]

  (parse-duration "5d")
  ;; => #object[java.time.Duration 0x439b8dd9 "PT120H"]

  (parse-duration "PT8760h0m0s")
  ;; => #object[java.time.Duration 0x7879db38 "PT8760H"]
  )

@ieugen
Copy link
Author

ieugen commented Apr 10, 2024

I managed to get the same behavior using filter and f/ok?
While this works. I still think it would be more readable if we had this as a construct in the library.

(defn parse-duration
  "Parse a duration from string.
   Return a ^java.lang.Duration on success.
   Retun nil when string is blank or empty.

   Throw exception on parse failure."
  [duration-str]
  (let [duration-tries [duration-str
                        (str "PT" duration-str)
                        (str "P" duration-str)]
        duration (filter f/ok? (map try-parse-duration duration-tries))]
    (if (empty? duration)
      (f/fail "Failed to parse duration %s" duration-str)
      (first duration)))

@vendethiel
Copy link
Contributor

As an optimization, you can stop at the first success with reduced. Something like this?

(defn f/try-any [f xs]
  (reduce
    (fn [_ x] (f/attempt reduced (f x)))
    xs))

@a13
Copy link

a13 commented Nov 21, 2024

hi, @ieugen you'll probably find this one interesting:

(defn some-ok
  "Like `clojure.core/some`, but returns the first non-failed value
  of (pred x) for any x in coll, else the final Failure.
  Usage:

  (some-ok #(if (odd? %)
              %
              (fail \"no odds\"))
         [2 4 8])"
  {:added "2.3"}
  [pred coll]
  (when-let-ok? [s (seq coll)]
    (or (pred (first s)) (recur pred (next s)))))

I thought about posting a PR with it a while back, but wasn't sure if it was needed upstream. CC @adambard what do you think?

With it, you can rewrite parse-duration as

(defn parse-duration
  [duration-str]
  (let [duration-tries [duration-str
                        (str "PT" duration-str)
                        (str "P" duration-str)]]
    (f/some-ok try-parse-duration duration-tries)))

As a nice side effect, it returns an exception object on failure, so you get all your stacktraces, as mentioned in #37.

Another function that I find useful is

(defn ex->failure
  [ex]
  (let [data (ex-data ex)
        cause (ex-cause ex)
        msg (ex-message ex)]
    (cond-> (fail msg)
      data (assoc :data data)
      cause (assoc :cause cause))))

It doesn't support stacktraces (mostly to simplify the code), but adds cause and ex-info data to the failure object.

It's probably worth adding to failjure as well, but I'm not sure

@ieugen
Copy link
Author

ieugen commented Nov 22, 2024

Thanks.
I'll check it out when I get back to the project I was using it in.

IMO Clojure does not seem to have a good solution for this type of programming where you check for failure at every step.

@a13
Copy link

a13 commented Nov 28, 2024

If clojure.core doesn't have it - just implement it yourself, I don't see a problem here :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants