fix(swap): display very small max values, fix scientific notation, handle decimal mismatch when changing tokens, display long numbers (#21388)

Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
Brian Sztamfater 2024-10-09 13:49:45 -03:00 committed by GitHub
parent a9e0b3dd6b
commit 5c44cb6399
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 283 additions and 134 deletions

View File

@ -75,3 +75,16 @@
(def fiat-amount (def fiat-amount
{:color colors/neutral-50}) {:color colors/neutral-50})
(def gradient-common
{:width 64
:position :absolute
:top 0
:bottom 0
:z-index 1})
(def gradient-start
(assoc gradient-common :left 0))
(def gradient-end
(assoc gradient-common :right 0))

View File

@ -11,6 +11,7 @@
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
quo.theme quo.theme
[react-native.core :as rn] [react-native.core :as rn]
[react-native.linear-gradient :as linear-gradient]
[schema.core :as schema])) [schema.core :as schema]))
(def ?schema (def ?schema
@ -18,6 +19,7 @@
[:catn [:catn
[:props [:props
[:map {:closed true} [:map {:closed true}
[:get-ref {:optional true} [:maybe fn?]]
[:type {:optional true} [:maybe [:enum :pay :receive]]] [:type {:optional true} [:maybe [:enum :pay :receive]]]
[:status {:optional true} [:maybe [:enum :default :typing :disabled :loading]]] [:status {:optional true} [:maybe [:enum :default :typing :disabled :loading]]]
[:token {:optional true} [:maybe :string]] [:token {:optional true} [:maybe :string]]
@ -42,26 +44,65 @@
[:container-style {:optional true} [:maybe :map]]]]] [:container-style {:optional true} [:maybe :map]]]]]
:any]) :any])
(def icon-size 32)
(def container-padding (* 2 (:padding (style/row-1 false))))
(def max-cursor-position 5)
(defn view-internal (defn view-internal
[{:keys [type status token value fiat-value show-approval-label? error? network-tag-props [{:keys [type status token value fiat-value show-approval-label? error? network-tag-props
approval-label-props default-value auto-focus? input-disabled? enable-swap? approval-label-props default-value auto-focus? input-disabled? enable-swap?
currency-symbol on-change-text show-keyboard? currency-symbol on-change-text show-keyboard? get-ref
container-style on-swap-press on-token-press on-max-press on-input-focus]}] container-style on-swap-press on-token-press on-max-press on-input-focus]}]
(let [theme (quo.theme/use-theme) (let [theme (quo.theme/use-theme)
pay? (= type :pay) pay? (= type :pay)
disabled? (= status :disabled) disabled? (= status :disabled)
loading? (= status :loading) loading? (= status :loading)
typing? (= status :typing) typing? (= status :typing)
controlled-input? (some? value) controlled-input? (some? value)
input-ref (rn/use-ref-atom nil) [container-width
set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref)) []) set-container-width] (rn/use-state)
focus-input (rn/use-callback (fn [] [overflow?
(some-> @input-ref set-overflow?] (rn/use-state false)
(oops/ocall "focus"))) [cursor-close-to-start?
[input-ref])] set-cursor-close-to-start?] (rn/use-state false)
[label-width
set-label-width] (rn/use-state 0)
input-ref (rn/use-ref-atom nil)
set-input-ref (rn/use-callback (fn [ref]
(reset! input-ref ref)
(when get-ref (get-ref ref)))
[])
focus-input (rn/use-callback (fn []
(some-> @input-ref
(oops/ocall "focus")))
[input-ref])
on-layout-container (rn/use-callback
(fn [e]
(let [width (oops/oget e "nativeEvent.layout.width")]
(set-container-width width))))
on-layout-text-input (rn/use-callback
(fn [e]
(let [width (oops/oget e "nativeEvent.layout.width")
max-width (- container-width
icon-size
container-padding
(* label-width 2))]
(set-overflow? (> width max-width))))
[container-width label-width])
on-layout-label (rn/use-callback
(fn [e]
(let [width (oops/oget e "nativeEvent.layout.width")]
(set-label-width width))))
on-selection-change (rn/use-callback
(fn [e]
(let [selection-start (oops/oget e
"nativeEvent.selection.start")]
(set-cursor-close-to-start?
(< selection-start max-cursor-position)))))]
[rn/view [rn/view
{:style container-style {:style container-style
:accessibility-label :swap-input} :accessibility-label :swap-input
:on-layout on-layout-container}
[rn/view {:style (style/content typing? theme)} [rn/view {:style (style/content typing? theme)}
[rn/view [rn/view
{:style (style/row-1 loading?)} {:style (style/row-1 loading?)}
@ -75,25 +116,43 @@
[rn/pressable [rn/pressable
{:style style/input-container {:style style/input-container
:on-press focus-input} :on-press focus-input}
[rn/text-input [rn/view {:style {:flex-shrink 1}}
(cond-> {:ref set-input-ref (when (and overflow? typing? (not cursor-close-to-start?))
:style (style/input disabled? error? theme) [linear-gradient/linear-gradient
:placeholder-text-color (colors/theme-colors colors/neutral-40 {:start {:x 0 :y 0}
colors/neutral-50 :end {:x 1 :y 0}
theme) :colors [(colors/theme-colors colors/white colors/neutral-100 theme)
:keyboard-type :numeric (colors/theme-colors colors/white-opa-10 colors/neutral-100-opa-10 theme)]
:editable (not input-disabled?) :style style/gradient-start}])
:auto-focus auto-focus? (when (and overflow? disabled?)
:on-focus on-input-focus [linear-gradient/linear-gradient
:on-change-text on-change-text {:start {:x 0 :y 0}
:show-soft-input-on-focus show-keyboard? :end {:x 1 :y 0}
:default-value default-value :colors [(colors/theme-colors colors/white-opa-10 colors/neutral-100-opa-10 theme)
:placeholder "0"} (colors/theme-colors colors/white colors/neutral-100 theme)]
controlled-input? (assoc :value value))] :style style/gradient-end}])
[rn/text-input
(cond-> {:ref set-input-ref
:style (style/input disabled? error? theme)
:placeholder-text-color (colors/theme-colors colors/neutral-40
colors/neutral-50
theme)
:keyboard-type :numeric
:editable (not input-disabled?)
:auto-focus auto-focus?
:on-focus on-input-focus
:on-change-text on-change-text
:on-layout on-layout-text-input
:on-selection-change on-selection-change
:show-soft-input-on-focus show-keyboard?
:default-value default-value
:placeholder "0"}
controlled-input? (assoc :value value))]]
[text/text [text/text
{:size :paragraph-2 {:size :paragraph-2
:weight :semi-bold :weight :semi-bold
:style (style/token-symbol theme)} :style (style/token-symbol theme)
:on-layout on-layout-label}
token]] token]]
(when (and pay? enable-swap?) (when (and pay? enable-swap?)
[buttons/button [buttons/button

View File

@ -47,7 +47,7 @@
(rf/dispatch [:wallet/bridge-select-token params]))}) (rf/dispatch [:wallet/bridge-select-token params]))})
(defn- action-swap (defn- action-swap
[{:keys [token token-symbol testnet-mode?]}] [{:keys [token asset-to-receive token-symbol testnet-mode?]}]
{:icon :i/swap {:icon :i/swap
:accessibility-label :swap :accessibility-label :swap
:label (i18n/label :t/swap) :label (i18n/label :t/swap)
@ -56,6 +56,7 @@
(rf/dispatch [:hide-bottom-sheet]) (rf/dispatch [:hide-bottom-sheet])
(rf/dispatch [:wallet.swap/start (rf/dispatch [:wallet.swap/start
{:asset-to-pay (or token {:symbol token-symbol}) {:asset-to-pay (or token {:symbol token-symbol})
:asset-to-receive asset-to-receive
:open-new-screen? true}]))}) :open-new-screen? true}]))})
(defn- action-manage-tokens (defn- action-manage-tokens
@ -75,23 +76,26 @@
(defn token-value-drawer (defn token-value-drawer
[token watch-only? entry-point] [token watch-only? entry-point]
(let [token-symbol (:token token) (let [token-symbol (:token token)
token-data (first (rf/sub [:wallet/current-viewing-account-tokens-filtered token-data (first (rf/sub [:wallet/current-viewing-account-tokens-filtered
{:query token-symbol}])) {:query token-symbol}]))
selected-account (rf/sub [:wallet/current-viewing-account-address]) selected-account (rf/sub [:wallet/current-viewing-account-address])
token-owners (rf/sub [:wallet/operable-addresses-with-token-symbol token-symbol]) token-owners (rf/sub [:wallet/operable-addresses-with-token-symbol token-symbol])
testnet-mode? (rf/sub [:profile/test-networks-enabled?]) testnet-mode? (rf/sub [:profile/test-networks-enabled?])
params (cond-> {:start-flow? true receive-token-symbol (if (= token-symbol "SNT") "ETH" "SNT")
:owners token-owners asset-to-receive (rf/sub [:wallet/token-by-symbol receive-token-symbol])
:testnet-mode? testnet-mode?} params (cond-> {:start-flow? true
selected-account :owners token-owners
(assoc :token token-data :testnet-mode? testnet-mode?
:stack-id :screen/wallet.accounts :asset-to-receive asset-to-receive}
:has-balance? (-> (get-in token [:values :fiat-unformatted-value]) selected-account
(money/greater-than (money/bignumber "0")))) (assoc :token token-data
(not selected-account) :stack-id :screen/wallet.accounts
(assoc :token-symbol token-symbol :has-balance? (-> (get-in token [:values :fiat-unformatted-value])
:stack-id :wallet-stack))] (money/greater-than (money/bignumber "0"))))
(not selected-account)
(assoc :token-symbol token-symbol
:stack-id :wallet-stack))]
[quo/action-drawer [quo/action-drawer
[(cond->> [(when (ff/enabled? ::ff/wallet.assets-modal-manage-tokens) [(cond->> [(when (ff/enabled? ::ff/wallet.assets-modal-manage-tokens)
(action-manage-tokens watch-only?)) (action-manage-tokens watch-only?))

View File

@ -14,7 +14,7 @@
[utils.number :as number])) [utils.number :as number]))
(rf/reg-event-fx :wallet.swap/start (rf/reg-event-fx :wallet.swap/start
(fn [{:keys [db]} [{:keys [network open-new-screen?] :as data}]] (fn [{:keys [db]} [{:keys [network asset-to-receive open-new-screen?] :as data}]]
(let [{:keys [wallet]} db (let [{:keys [wallet]} db
test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?]) test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?])
account (swap-utils/wallet-account wallet) account (swap-utils/wallet-account wallet)
@ -25,12 +25,6 @@
:account account :account account
:test-networks-enabled? test-networks-enabled? :test-networks-enabled? test-networks-enabled?
:token-symbol (get-in data [:asset-to-pay :symbol])})) :token-symbol (get-in data [:asset-to-pay :symbol])}))
asset-to-receive (or (:asset-to-receive data)
(swap-utils/select-default-asset-to-receive
{:wallet wallet
:account account
:test-networks-enabled? test-networks-enabled?
:asset-to-pay asset-to-pay}))
network' (or network network' (or network
(swap-utils/select-network asset-to-pay))] (swap-utils/select-network asset-to-pay))]
{:db (-> db {:db (-> db
@ -137,8 +131,6 @@
:amount amount :amount amount
:amount-hex amount-in-hex :amount-hex amount-in-hex
:loading-swap-proposal? true) :loading-swap-proposal? true)
:always
(dissoc :error-response)
clean-approval-transaction? clean-approval-transaction?
(dissoc :approval-transaction-id :approved-amount :swap-proposal))) (dissoc :approval-transaction-id :approved-amount :swap-proposal)))
:json-rpc/call [{:method "wallet_getSuggestedRoutesAsync" :json-rpc/call [{:method "wallet_getSuggestedRoutesAsync"

View File

@ -20,10 +20,15 @@
(defn- assets-view (defn- assets-view
[search-text on-change-text] [search-text on-change-text]
(let [on-token-press (fn [token] (let [snt-token (rf/sub [:wallet/token-by-symbol "SNT"])
(rf/dispatch [:wallet.swap/start eth-token (rf/sub [:wallet/token-by-symbol "ETH"])
{:asset-to-pay token on-token-press (fn [token]
:open-new-screen? false}]))] (let [pay-token-symbol (:symbol token)
asset-to-receive (if (= pay-token-symbol "SNT") eth-token snt-token)]
(rf/dispatch [:wallet.swap/start
{:asset-to-pay token
:asset-to-receive asset-to-receive
:open-new-screen? false}])))]
[:<> [:<>
[search-input search-text on-change-text] [search-input search-text on-change-text]
[asset-list/view [asset-list/view

View File

@ -1,27 +1,29 @@
(ns status-im.contexts.wallet.swap.setup-swap.view (ns status-im.contexts.wallet.swap.setup-swap.view
(:require [clojure.string :as string] (:require
[quo.core :as quo] [clojure.string :as string]
[quo.foundations.colors :as colors] [oops.core :as oops]
[quo.theme :as quo.theme] [quo.core :as quo]
[react-native.core :as rn] [quo.foundations.colors :as colors]
[react-native.platform :as platform] [quo.theme :as quo.theme]
[react-native.safe-area :as safe-area] [react-native.core :as rn]
[status-im.common.controlled-input.utils :as controlled-input] [react-native.platform :as platform]
[status-im.common.events-helper :as events-helper] [react-native.safe-area :as safe-area]
[status-im.constants :as constants] [status-im.common.controlled-input.utils :as controlled-input]
[status-im.contexts.wallet.common.account-switcher.view :as account-switcher] [status-im.common.events-helper :as events-helper]
[status-im.contexts.wallet.common.utils :as utils] [status-im.constants :as constants]
[status-im.contexts.wallet.sheets.buy-token.view :as buy-token] [status-im.contexts.wallet.common.account-switcher.view :as account-switcher]
[status-im.contexts.wallet.sheets.select-asset.view :as select-asset] [status-im.contexts.wallet.common.utils :as utils]
[status-im.contexts.wallet.sheets.slippage-settings.view :as slippage-settings] [status-im.contexts.wallet.sheets.buy-token.view :as buy-token]
[status-im.contexts.wallet.swap.setup-swap.style :as style] [status-im.contexts.wallet.sheets.select-asset.view :as select-asset]
[status-im.contexts.wallet.swap.utils :as swap-utils] [status-im.contexts.wallet.sheets.slippage-settings.view :as slippage-settings]
[utils.debounce :as debounce] [status-im.contexts.wallet.swap.setup-swap.style :as style]
[utils.i18n :as i18n] [status-im.contexts.wallet.swap.utils :as swap-utils]
[utils.money :as money] [utils.debounce :as debounce]
[utils.number :as number] [utils.i18n :as i18n]
[utils.re-frame :as rf] [utils.money :as money]
[utils.string :as utils.string])) [utils.number :as number]
[utils.re-frame :as rf]
[utils.string :as utils.string]))
(def ^:private default-text-for-unfocused-input "0.00") (def ^:private default-text-for-unfocused-input "0.00")
@ -97,13 +99,18 @@
approved-amount (rf/sub [:wallet/swap-approved-amount]) approved-amount (rf/sub [:wallet/swap-approved-amount])
error-response (rf/sub [:wallet/swap-error-response]) error-response (rf/sub [:wallet/swap-error-response])
asset-to-pay (rf/sub [:wallet/token-by-symbol pay-token-symbol]) asset-to-pay (rf/sub [:wallet/token-by-symbol pay-token-symbol])
overlay-shown? (boolean (:sheets (rf/sub [:bottom-sheet])))
input-ref (rn/use-ref-atom nil)
set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref)))
pay-input-num-value (controlled-input/value-numeric input-state) pay-input-num-value (controlled-input/value-numeric input-state)
pay-input-amount (controlled-input/input-value input-state) pay-input-amount (controlled-input/input-value input-state)
pay-token-decimals (:decimals asset-to-pay) pay-token-decimals (:decimals asset-to-pay)
pay-token-balance-selected-chain (get-in asset-to-pay pay-token-balance-selected-chain (number/convert-to-whole-number
[:balances-per-chain (get-in asset-to-pay
(:chain-id network) :balance] [:balances-per-chain
0) (:chain-id network) :raw-balance]
0)
pay-token-decimals)
pay-token-fiat-value (str pay-token-fiat-value (str
(utils/calculate-token-fiat-value (utils/calculate-token-fiat-value
{:currency currency {:currency currency
@ -111,10 +118,15 @@
:token asset-to-pay})) :token asset-to-pay}))
available-crypto-limit (money/bignumber available-crypto-limit (money/bignumber
pay-token-balance-selected-chain) pay-token-balance-selected-chain)
display-decimals (min pay-token-decimals
constants/min-token-decimals-to-display)
available-crypto-limit-display (number/remove-trailing-zeroes available-crypto-limit-display (number/remove-trailing-zeroes
(.toFixed available-crypto-limit (.toFixed available-crypto-limit display-decimals))
(min pay-token-decimals available-crypto-limit-display (if (and (= available-crypto-limit-display "0")
constants/min-token-decimals-to-display))) (money/greater-than available-crypto-limit
(money/bignumber 0)))
(number/small-number-threshold display-decimals)
available-crypto-limit-display)
approval-amount-required-num (when approval-amount-required approval-amount-required-num (when approval-amount-required
(str (number/hex->whole approval-amount-required (str (number/hex->whole approval-amount-required
pay-token-decimals))) pay-token-decimals)))
@ -122,8 +134,7 @@
(money/greater-than (money/greater-than
(money/bignumber pay-input-amount) (money/bignumber pay-input-amount)
available-crypto-limit)) available-crypto-limit))
(money/equal-to (money/bignumber (money/equal-to available-crypto-limit
available-crypto-limit-display)
(money/bignumber 0))) (money/bignumber 0)))
valid-pay-input? (and valid-pay-input? (and
(not (string/blank? (not (string/blank?
@ -141,8 +152,15 @@
(fn [] (fn []
(request-fetch-swap-proposal)) (request-fetch-swap-proposal))
[pay-input-amount]) [pay-input-amount])
(rn/use-effect
(fn []
(when-not overlay-shown?
(some-> @input-ref
(oops/ocall "focus"))))
[overlay-shown?])
[quo/swap-input [quo/swap-input
{:type :pay {:get-ref set-input-ref
:type :pay
:error? pay-input-error? :error? pay-input-error?
:token pay-token-symbol :token pay-token-symbol
:customization-color :blue :customization-color :blue
@ -156,7 +174,7 @@
:else :disabled) :else :disabled)
:currency-symbol currency-symbol :currency-symbol currency-symbol
:on-token-press on-token-press :on-token-press on-token-press
:on-max-press #(on-max-press (str available-crypto-limit)) :on-max-press #(on-max-press (str pay-token-balance-selected-chain))
:on-input-focus on-input-focus :on-input-focus on-input-focus
:value pay-input-amount :value pay-input-amount
:fiat-value pay-token-fiat-value :fiat-value pay-token-fiat-value
@ -315,10 +333,12 @@
network (rf/sub [:wallet/swap-network]) network (rf/sub [:wallet/swap-network])
pay-input-amount (controlled-input/input-value pay-input-state) pay-input-amount (controlled-input/input-value pay-input-state)
pay-token-decimals (:decimals asset-to-pay) pay-token-decimals (:decimals asset-to-pay)
pay-token-balance-selected-chain (get-in asset-to-pay pay-token-balance-selected-chain (number/convert-to-whole-number
[:balances-per-chain (get-in asset-to-pay
(:chain-id network) :balance] [:balances-per-chain
0) (:chain-id network) :raw-balance]
0)
pay-token-decimals)
pay-input-error? (and (not (string/blank? pay-input-amount)) pay-input-error? (and (not (string/blank? pay-input-amount))
(money/greater-than (money/greater-than
(money/bignumber pay-input-amount) (money/bignumber pay-input-amount)
@ -420,13 +440,17 @@
(when (and swap-amount refetch-interval) (when (and swap-amount refetch-interval)
(js/clearTimeout @refetch-interval) (js/clearTimeout @refetch-interval)
(reset! refetch-interval nil)) (reset! refetch-interval nil))
(if (and swap-amount (not= swap-amount pay-input-amount)) (cond (and swap-amount (not= swap-amount pay-input-amount))
(set-pay-input-state (set-pay-input-state
(fn [input-state] (fn [input-state]
(controlled-input/set-input-value (controlled-input/set-input-value
input-state input-state
swap-amount))) swap-amount)))
(refetch-swap-proposal))))) (and pay-input-amount
(not (number/valid-decimal-count? pay-input-amount (:decimals asset-to-pay))))
(set-pay-input-state controlled-input/delete-all)
:else
(refetch-swap-proposal)))))
[asset-to-pay]) [asset-to-pay])
(rn/use-effect (rn/use-effect
refetch-swap-proposal refetch-swap-proposal
@ -461,7 +485,7 @@
:on-token-press #(rf/dispatch [:show-bottom-sheet {:content receive-token-bottom-sheet}]) :on-token-press #(rf/dispatch [:show-bottom-sheet {:content receive-token-bottom-sheet}])
:on-input-focus #(set-pay-input-focused? false)}]] :on-input-focus #(set-pay-input-focused? false)}]]
[rn/view {:style style/footer-container} [rn/view {:style style/footer-container}
[alert-banner {:pay-input-error? pay-input-error?}] (when-not loading-swap-proposal? [alert-banner {:pay-input-error? pay-input-error?}])
(when (or loading-swap-proposal? swap-proposal) (when (or loading-swap-proposal? swap-proposal)
[transaction-details]) [transaction-details])
[action-button {:on-press on-review-swap-press}]] [action-button {:on-press on-review-swap-press}]]

View File

@ -492,7 +492,7 @@
(rf/reg-sub (rf/reg-sub
:wallet/token-by-symbol :wallet/token-by-symbol
:<- [:wallet/current-viewing-account] :<- [:wallet/current-viewing-account-or-default]
:<- [:wallet/network-details] :<- [:wallet/network-details]
(fn [[{:keys [tokens]} networks] [_ token-symbol chain-ids]] (fn [[{:keys [tokens]} networks] [_ token-symbol chain-ids]]
(->> (utils/tokens-with-balance tokens networks chain-ids) (->> (utils/tokens-with-balance tokens networks chain-ids)

View File

@ -2,7 +2,8 @@
(:require [clojure.string :as string] (:require [clojure.string :as string]
[native-module.core :as native-module] [native-module.core :as native-module]
[utils.hex :as utils.hex] [utils.hex :as utils.hex]
[utils.money :as utils.money])) [utils.money :as utils.money]
[utils.money :as money]))
(defn naive-round (defn naive-round
"Quickly and naively round number `n` up to `decimal-places`. "Quickly and naively round number `n` up to `decimal-places`.
@ -18,17 +19,6 @@
(/ (Math/round (* n scale)) (/ (Math/round (* n scale))
scale))) scale)))
(defn convert-to-whole-number
"Converts a fractional `amount` to its corresponding whole number representation
by dividing it by 10 raised to the power of `decimals`. This is often used in financial
calculations where amounts are stored in their smallest units (e.g., cents) and need
to be converted to their whole number equivalents (e.g., dollars).
Example usage:
(convert-to-whole-number 12345 2) ; => 123.45"
[amount decimals]
(/ amount (Math/pow 10 decimals)))
(defn parse-int (defn parse-int
"Parses `n` as an integer. Defaults to zero or `default` instead of NaN." "Parses `n` as an integer. Defaults to zero or `default` instead of NaN."
([n] ([n]
@ -65,15 +55,44 @@
"") "")
"")))) ""))))
(defn convert-to-whole-number
"Converts a fractional `amount` to its corresponding whole number representation
by dividing it by 10 raised to the power of `decimals`. This is often used in financial
calculations where amounts are stored in their smallest units (e.g., cents) and need
to be converted to their whole number equivalents (e.g., dollars).
Example usage:
(convert-to-whole-number 12345 2) ; => 123.45"
[amount decimals]
(-> amount
(/ (Math/pow 10 decimals))
(.toFixed decimals)
remove-trailing-zeroes))
(defn hex->whole (defn hex->whole
[num decimals] [num decimals]
(-> num (-> num
utils.hex/normalize-hex utils.hex/normalize-hex
native-module/hex-to-number native-module/hex-to-number
(convert-to-whole-number decimals))) (convert-to-whole-number decimals)
money/bignumber))
(defn to-fixed (defn to-fixed
[num decimals] [num decimals]
(-> num (-> num
(utils.money/to-fixed decimals) (utils.money/to-fixed decimals)
remove-trailing-zeroes)) remove-trailing-zeroes))
(defn small-number-threshold
"Receives a decimal count and returns a string like '<0.001' if the decimal count is 3,
'<0.000001' if the decimal count is 6, etc."
[decimal-count]
(if (> decimal-count 0)
(str "<0." (apply str (repeat (dec decimal-count) "0")) "1")
"0"))
(defn valid-decimal-count?
"Returns false if the number has more decimals than the decimal count, otherwise true."
[num decimal-count]
(let [decimal-part (second (string/split (str num) #"\."))]
(or (nil? decimal-part) (<= (count decimal-part) decimal-count))))

View File

@ -5,23 +5,23 @@
(deftest convert-to-whole-number-test (deftest convert-to-whole-number-test
(testing "correctly converts fractional amounts to whole numbers" (testing "correctly converts fractional amounts to whole numbers"
(is (= 123.45 (utils.number/convert-to-whole-number 12345 2))) (is (= "123.45" (utils.number/convert-to-whole-number 12345 2)))
(is (= 1.2345 (utils.number/convert-to-whole-number 12345 4))) (is (= "1.2345" (utils.number/convert-to-whole-number 12345 4)))
(is (= 12345.0 (utils.number/convert-to-whole-number 1234500 2))) (is (= "12345" (utils.number/convert-to-whole-number 1234500 2)))
(is (= 0.123 (utils.number/convert-to-whole-number 123 3))) (is (= "0.123" (utils.number/convert-to-whole-number 123 3)))
(is (= 1000.0 (utils.number/convert-to-whole-number 1000000 3)))) (is (= "1000" (utils.number/convert-to-whole-number 1000000 3))))
(testing "handles zero decimals" (testing "handles zero decimals"
(is (= 12345 (utils.number/convert-to-whole-number 12345 0)))) (is (= "12345" (utils.number/convert-to-whole-number 12345 0))))
(testing "handles negative amounts" (testing "handles negative amounts"
(is (= -123.45 (utils.number/convert-to-whole-number -12345 2))) (is (= "-123.45" (utils.number/convert-to-whole-number -12345 2)))
(is (= -1.2345 (utils.number/convert-to-whole-number -12345 4))) (is (= "-1.2345" (utils.number/convert-to-whole-number -12345 4)))
(is (= -0.123 (utils.number/convert-to-whole-number -123 3)))) (is (= "-0.123" (utils.number/convert-to-whole-number -123 3))))
(testing "handles zero amount" (testing "handles zero amount"
(is (= 0 (utils.number/convert-to-whole-number 0 2))) (is (= "0" (utils.number/convert-to-whole-number 0 2)))
(is (= 0 (utils.number/convert-to-whole-number 0 0))))) (is (= "0" (utils.number/convert-to-whole-number 0 0)))))
(deftest parse-int-test (deftest parse-int-test
(testing "defaults to zero" (testing "defaults to zero"
@ -50,3 +50,36 @@
(is (= 6 (utils.number/parse-float "6"))) (is (= 6 (utils.number/parse-float "6")))
(is (= 6.99 (utils.number/parse-float "6.99" 0))) (is (= 6.99 (utils.number/parse-float "6.99" 0)))
(is (= -6.9 (utils.number/parse-float "-6.9" 0))))) (is (= -6.9 (utils.number/parse-float "-6.9" 0)))))
(deftest small-number-threshold-test
(testing "correctly generates threshold strings based on decimal count"
(is (= "<0.1" (utils.number/small-number-threshold 1)))
(is (= "<0.01" (utils.number/small-number-threshold 2)))
(is (= "<0.001" (utils.number/small-number-threshold 3)))
(is (= "<0.000001" (utils.number/small-number-threshold 6)))
(is (= "<0.0000000001" (utils.number/small-number-threshold 10)))
(is (= "<0.000000000000000001" (utils.number/small-number-threshold 18)))
(is (= "<0.000000000000000000001" (utils.number/small-number-threshold 21))))
(testing "handles edge cases for decimal count"
(is (= "0" (utils.number/small-number-threshold 0)))
(is (= "0" (utils.number/small-number-threshold -1)))))
(deftest valid-decimal-count?-test
(testing "valid decimal count check for numbers with varying decimals"
(is (true? (utils.number/valid-decimal-count? 123 2)))
(is (true? (utils.number/valid-decimal-count? 123 0)))
(is (true? (utils.number/valid-decimal-count? 123.45 2)))
(is (true? (utils.number/valid-decimal-count? 123.4 2)))
(is (true? (utils.number/valid-decimal-count? 123.456 3)))
(is (false? (utils.number/valid-decimal-count? 123.456 2)))
(is (false? (utils.number/valid-decimal-count? 123.456789 4)))
(is (true? (utils.number/valid-decimal-count? 123.0 1)))
(is (true? (utils.number/valid-decimal-count? -123.45 2)))
(is (false? (utils.number/valid-decimal-count? -123.4567 3)))
(is (true? (utils.number/valid-decimal-count? 1234567890.12 2)))
(is (false? (utils.number/valid-decimal-count? 1234567890.12345 3)))))