-
Notifications
You must be signed in to change notification settings - Fork 5
/
currency.clj
1722 lines (1492 loc) · 70.1 KB
/
currency.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(ns io.randomseed.bankster.currency
^{:doc "Bankster library, currency operations."
:author "Paweł Wilk"
:added "1.0.0"}
(:refer-clojure :rename {ns core-ns new core-new symbol core-symbol
name core-name update core-update})
(:require [trptr.java-wrapper.locale :as l]
[smangler.api :as sm]
[clojure.string :as str]
[io.randomseed.bankster :refer :all]
[io.randomseed.bankster.config :as config]
[io.randomseed.bankster.registry :as registry]
[io.randomseed.bankster.scale :as scale]
[io.randomseed.bankster.util.map :as map]
[io.randomseed.bankster.util :refer :all])
(:import [io.randomseed.bankster Currency Registry]
[java.math RoundingMode]
[java.text NumberFormat DecimalFormat DecimalFormatSymbols]
[java.util Locale]))
;;
;; Constants.
;;
(def ^{:tag 'long :const true :added "1.0.0"}
no-numeric-id
"Expresses the value of currency's numeric ID which does not exist."
(long -1))
(def ^{:tag 'int :const true :added "1.0.0"}
auto-scaled
"Expresses the scale of a currency which is automatic and not limited to certain
decimal places."
(int -1))
;;
;; Default currency.
;;
(def ^{:added "1.0.0" :tag Currency :dynamic true}
*default*
"Default currency unit to be applied when creating Money objects without the currency
specified."
nil)
;;
;; Auto-scaling predicate for scale.
;;
(defn val-auto-scaled?
"Returns true if the given scale is equal to auto-scaled."
{:added "1.0.0"}
[scale]
(= auto-scaled scale))
;;
;; Currency constructor
;;
(declare map->new)
(defn new-currency
"Creates new currency record from values passed as arguments."
{:added "1.0.0" :tag Currency}
(^Currency [id]
(if (map? id)
(map->new id)
(new-currency id nil nil nil nil nil)))
(^Currency [id numeric-id] (new-currency id numeric-id nil nil nil nil))
(^Currency [id numeric-id scale] (new-currency id numeric-id scale nil nil nil))
(^Currency [id numeric-id scale kind] (new-currency id numeric-id scale kind nil nil))
(^Currency [id numeric-id scale kind domain] (new-currency id numeric-id scale kind domain nil))
(^Currency [id numeric-id scale kind domain weight]
(when (some? id)
(let [numeric-id (or numeric-id no-numeric-id)
scale (or scale auto-scaled)
weight (or weight 0)
ns-domain (keyword (try-upper-case (namespace id)))
domain (if (nil? domain) ns-domain
(keyword
(str/upper-case
(if (ident? domain)
(core-name domain)
(let [d (str domain)] (when (seq d) d))))))]
(when (and (some? ns-domain) (not= domain ns-domain))
(throw (ex-info
(str "Currency domain should reflect its namespace (upper-cased) if the namespace is set.")
{:id id :domain domain :namespace (namespace id)})))
(Currency. (keyword id)
(long numeric-id)
(int scale)
(keyword kind)
(keyword domain)
(int weight))))))
(defn map->new
"Creates new currency record from a map."
{:added "1.0.0" :tag Currency}
[^clojure.lang.IPersistentMap m]
(when (and (some? m) (> (count m) 0))
(let [id (:id m)
nr (or (:nr m) (:numeric m) no-numeric-id)
sc (or (:sc m) (:scale m) auto-scaled)
kind (or (:ki m) (:kind m))
domain (or (:do m) (:domain m))
weight (or (:we m) (:weight m) (int 0))]
(when-some [c ^Currency (new-currency id nr sc kind domain weight)]
(if (> (count m) 1)
(merge c (dissoc m :id :nr :numeric :sc :scale :ki :kind :do :domain :we :weight))
c)))))
(def ^{:tag Currency
:arglists '(^Currency [id]
^Currency [id numeric-id]
^Currency [id numeric-id scale]
^Currency [id numeric-id scale kind]
^Currency [id numeric-id scale kind domain]
^Currency [id numeric-id scale kind domain weight])}
new
"Alias for new-currency."
new-currency)
;;
;; Monetary protocol.
;;
(defprotocol ^{:added "1.0.0"} Monetary
"The Monetary protocol describes basic operations on currencies. It uses single
dispatch to allow currencies to be expressed with different kinds of
data (keywords, symbols, strings, native Currency objects etc.)."
(^{:tag clojure.lang.Keyword :added "1.0.0"}
id
[id] [id registry]
"Returns a unique identifier of the given currency as a keyword. The currency can
be expressed as a Currency object, a keyword, a string or any other type which is
recognized by the `unit` protocol method.
The default registry role is advisory here. If the registry argument is not
given (or it is nil) and the dynamic variable
`io.randomseed.bankster.registry/*default*` does not refer to a truthy value then
the ID will be returned regardless of whether the currency exists in a registry, by
simply converting it to a keyword or getting a field from a Currency object. Still,
the default registry will be consulted to resolve possible currency code. For
instance: if `BTC` is a currency code of a registered currency identified as
`:crypto/BTC` then the resulting value for `:BTC` will be `:crypto/BTC`; but for
`:BLABLA` (which does not exist in a registry) the resulting value will be
`:BLABLA`.
If a registry is given (or a dynamic registry is set) then trying to use a
non-existing currency will cause an exception to be thrown.")
(^{:tag io.randomseed.bankster.Currency :added "1.0.0"}
of-id
[id] [id registry]
"Returns a currency object for the given ID and a registry. If the registry is not
given, it will use the global one, but will first try a registry bound to the
`io.randomseed.bankster.registry/*default*` dynamic variable.")
(^{:tag io.randomseed.bankster.Currency :added "1.0.2"}
unit
[id] [id registry]
"Returns a currency object for the given ID or currency code. If the registry is
not given, it will try a registry bound to the
`io.randomseed.bankster.registry/*default*` dynamic variable and if it is not
set (or is falsy) it will use the global registry.
If a Currency object is passed, it will be returned as is without consulting the
registry, unless the registry is given (and not nil) or the dynamic registry is
set. In such case the currency will be obtained from the registry on a basis of the
ID extracted from the given currency object.
If the registry is given (or dynamic registry is set) and the currency does not
exist in a registry an exception will be thrown, regardless of whether a currency
object was passed.
Explicitly passing nil as a second argument when a Currency object is given can
speed things up a bit by bypassing dynamic variable check.")
(^{:tag Boolean :added "1.0.0"}
defined?
[id] [id registry]
"Returns true if the given currency (identified by its ID) exists in a
registry. If the registry is not given, the global one will be used, trying a
dynamic registry bound to the registry/*default* first.")
(^{:tag Boolean :added "1.0.2"}
present?
[id] [id registry]
"Returns true if a currency of the given currency code or ID exists in a
registry. If the registry is not given, the global one will be used, trying a
dynamic registry bound to the registry/*default* first.")
(^{:tag Boolean :added "1.0.0"}
same-ids?
[a b] [a b registry]
"Returns true if two currencies have the same ID. If the registry is not given,
it will use the global one, but will first try a dynamic registry bound to the
`io.randomseed.bankster.registry/*default*` dynamic variable."))
;;
;; Currency querying functions, Monetary implementation.
;;
(extend-protocol Monetary
Currency
(of-id
(^Currency [currency]
(if-let [r registry/*default*] (of-id currency r) currency))
(^Currency [currency ^Registry registry]
(if (nil? registry) currency
(unit (.id ^Currency currency) registry))))
(unit
(^Currency [currency]
(if-let [r registry/*default*] (unit currency r) currency))
(^Currency [currency ^Registry registry]
(if (nil? registry) currency
(of-id (.id ^Currency currency) ^Registry registry))))
(id
(^clojure.lang.Keyword [currency] (.id ^Currency currency))
(^clojure.lang.Keyword [currency ^Registry registry] (.id ^Currency currency)))
(defined?
(^Boolean [currency]
(contains? (registry/currency-id->currency) (.id ^Currency currency)))
(^Boolean [currency ^Registry registry]
(contains? (registry/currency-id->currency registry) (.id ^Currency currency))))
(present?
(^Boolean [currency]
(present? (.id ^Currency currency)))
(^Boolean [currency ^Registry registry]
(present? (.id ^Currency currency) registry)))
(same-ids?
(^Boolean [a b] (= (.id ^Currency a) (id b)))
(^Boolean [a b ^Registry registry] (= (.id ^Currency a) (id b registry))))
Number
(of-id
(^Currency [num]
(of-id num (registry/get)))
(^Currency [num ^Registry registry]
(or (get (registry/currency-nr->currency registry) num)
(throw (ex-info
(str "Currency with the numeric ID of " num " not found in a registry.")
{:registry registry})))))
(unit
(^Currency [num]
(of-id num (registry/get)))
(^Currency [num ^Registry registry]
(of-id num registry))
(^Currency [num ^Registry registry _]
(of-id num registry)))
(id
(^clojure.lang.Keyword [num]
(id (long num) (registry/get)))
(^clojure.lang.Keyword [num ^Registry registry]
(if-some [c (get (registry/currency-nr->currency registry) num)]
(.id ^Currency c)
(throw (ex-info
(str "Currency with the numeric ID of " num " not found in a registry.")
{:registry registry})))))
(defined?
(^Boolean [num]
(contains? (registry/currency-nr->currency) num))
(^Boolean [num ^Registry registry]
(contains? (registry/currency-nr->currency registry) num)))
(present?
(^Boolean [num]
(contains? (registry/currency-nr->currency) num))
(^Boolean [num ^Registry registry]
(contains? (registry/currency-nr->currency registry) num)))
(same-ids?
(^Boolean [a b]
(let [r (registry/get)]
(if-some [^Currency c (get (registry/currency-nr->currency r) a)]
(= (.id ^Currency c) (id b))
(throw (ex-info
(str "Currency with the numeric ID of " num " not found in a registry.")
{:registry r})))))
(^Boolean [a b ^Registry registry]
(if-some [^Currency c (get (registry/currency-nr->currency registry) a)]
(= (.id ^Currency c) (id b registry))
(throw (ex-info
(str "Currency with the numeric ID of " num " not found in a registry.")
{:registry registry})))))
clojure.lang.Keyword
(of-id
(^Currency [id]
(of-id id (registry/get)))
(^Currency [id ^Registry registry]
(or (get (registry/currency-id->currency registry) id)
(throw (ex-info
(str "Currency " (core-symbol id) " not found in a registry.")
{:registry registry})))))
(unit
(^Currency [id]
(unit id (registry/get)))
(^Currency [id ^Registry registry]
(or (if (namespace id)
(get (registry/currency-id->currency registry) id)
(first (get (registry/currency-code->currencies registry) id)))
(throw (ex-info
(str "Currency " (core-symbol id) " not found in a registry.")
{:registry registry})))))
(id
(^clojure.lang.Keyword [c]
(if-let [r registry/*default*]
(.id ^Currency (unit ^clojure.lang.Keyword c ^Registry r))
(if (namespace c) c
(if-some [cur (first (get (registry/currency-code->currencies) c))]
(.id ^Currency cur) c))))
(^clojure.lang.Keyword [c ^Registry registry]
(if (nil? registry) c
(.id ^Currency (unit ^clojure.lang.Keyword c ^Registry registry)))))
(defined?
(^Boolean [id]
(contains? (registry/currency-id->currency) id))
(^Boolean [id ^Registry registry]
(contains? (registry/currency-id->currency registry) id)))
(present?
(^Boolean [id]
(if (namespace id)
(contains? (registry/currency-id->currency) id)
(contains? (registry/currency-code->currencies) id)))
(^Boolean [id ^Registry registry]
(if (namespace id)
(contains? (registry/currency-id->currency registry) id)
(contains? (registry/currency-code->currencies registry) id))))
(same-ids?
(^Boolean [a b]
(if-let [r registry/*default*]
(= (id a r) (id b r))
(= (id a nil) (id b nil))))
(^Boolean [a b ^Registry registry]
(= (id a registry) (id b registry))))
String
(of-id
(^Currency [id] (of-id (keyword id)))
(^Currency [id, ^Registry registry] (of-id (keyword id) registry)))
(unit
(^Currency [id] (unit (keyword id)))
(^Currency [id ^Registry registry] (unit (keyword id) registry)))
(id
(^clojure.lang.Keyword [c] (id (keyword c)))
(^clojure.lang.Keyword [c ^Registry registry] (id (keyword c) registry)))
(defined?
(^Boolean [id]
(contains? (registry/currency-id->currency) (keyword id)))
(^Boolean [id ^Registry registry]
(contains? (registry/currency-id->currency registry) (keyword id))))
(present?
(^Boolean [id]
(present? (keyword id)))
(^Boolean [id ^Registry registry]
(present? (keyword id) registry)))
(same-ids?
(^Boolean [a b] (= (keyword a) (id b)))
(^Boolean [a b ^Registry registry] (= (keyword a) (id b registry))))
clojure.lang.Symbol
(of-id
(^Currency [id] (of-id (keyword id)))
(^Currency [id, ^Registry registry] (of-id (keyword id) registry)))
(unit
(^Currency [id] (unit (keyword id)))
(^Currency [id ^Registry registry] (unit (keyword id) registry)))
(id
(^clojure.lang.Keyword [c] (id (keyword c)))
(^clojure.lang.Keyword [c ^Registry registry] (id (keyword c) registry)))
(defined?
(^Boolean [id]
(contains? (registry/currency-id->currency) (keyword id)))
(^Boolean [id, ^Registry registry]
(contains? (registry/currency-id->currency registry) (keyword id))))
(present?
(^Boolean [id]
(present? (keyword id)))
(^Boolean [id ^Registry registry]
(present? (keyword id) registry)))
(same-ids?
(^Boolean [a b] (= (keyword a) (id b)))
(^Boolean [a b ^Registry registry] (= (keyword a) (id b registry))))
clojure.lang.IPersistentMap
(of-id
(^Currency [m] (map->new m))
(^Currency [m ^Registry registry] (map->new m)))
(unit
(^Currency [m] (map->new m))
(^Currency [m ^Registry registry] (map->new m)))
(id
(^clojure.lang.Keyword [m] (id (keyword (:id m))))
(^clojure.lang.Keyword [m, ^Registry registry] (id (keyword (:id m)) registry)))
(defined?
(^Boolean [m]
(contains? (registry/currency-id->currency) (keyword (:id m))))
(^Boolean [m ^Registry registry]
(contains? (registry/currency-id->currency registry) (keyword (:id m)))))
(present?
(^Boolean [m]
(present? (keyword (:id m))))
(^Boolean [m ^Registry registry]
(present? (keyword (:id m)) registry)))
(same-ids?
(^Boolean [m b] (= (keyword (:id m)) (id b)))
(^Boolean [m b ^Registry registry] (= (keyword (:id m)) (id b registry))))
nil
(of-id
([currency] (if-some [d *default*] (of-id d) nil))
([currency ^Registry registry] (if-some [d *default*] (of-id d) nil)))
(unit
([currency] (if-some [d *default*] (unit d) nil))
([currency ^Registry registry] (if-some [d *default*] (unit d) nil)))
(id
(^clojure.lang.Keyword [currency] nil)
(^clojure.lang.Keyword [currency ^Registry registry] nil))
(defined?
(^Boolean [currency] false)
(^Boolean [currency ^Registry registry] false))
(present?
(^Boolean [currency] false)
(^Boolean [currency ^Registry registry] false))
(same-ids?
(^Boolean [a b] false)
(^Boolean [a b ^Registry registry] false)))
(defn parse-currency-id
"Internal helper which transforms currency IDs into keywords."
{:no-doc true :added "1.0.0"}
[c]
(if (and (symbol? c) (defined? c))
(keyword c) c))
(defn parse-currency-code
"Internal helper which transforms currency codes into keywords."
{:no-doc true :added "1.0.2"}
[c]
(if (and (symbol? c) (present? c))
(keyword c) c))
(defmacro of
"Returns a currency for the given value by querying the given registry or a global
registry, which may be shadowed by the value of registry/*default* (see
registry/with or with-registry)."
{:added "1.0.0"}
([currency]
(let [cur# (parse-currency-code currency)]
`(unit ~cur#)))
([currency registry]
(let [cur# (parse-currency-code currency)]
`(unit ~cur# ~registry))))
;;
;; Currency properties.
;;
(defn nr
"Returns currency numeric ID as a long number. For currencies without the assigned
number it will return nil. Locale argument is ignored."
{:tag Long :added "1.0.0"}
(^Long [c]
(let [n (.numeric ^Currency (unit c))]
(when-not (= n no-numeric-id) n)))
(^Long [c ^Registry registry]
(let [n (.numeric ^Currency (unit c registry))]
(when-not (= n no-numeric-id) n)))
(^Long [c ^Registry locale registry]
(let [n (.numeric ^Currency (unit c registry))]
(when-not (= n no-numeric-id) n))))
(def ^{:tag Long
:arglists '(^Long [c]
^Long [c ^Registry registry]
^Long [c locale ^Registry registry])}
numeric-id
"Alias for nr."
nr)
(def ^{:tag Long
:arglists '(^Long [c]
^Long [c ^Registry registry]
^Long [c locale ^Registry registry])}
numeric
"Alias for nr."
nr)
(defn sc
"Returns currency scale (decimal places) as an integer number. For currencies without
the assigned decimal places it will return nil (the value of auto-scaled). Locale
argument is ignored."
{:tag Integer :added "1.0.0"}
(^Integer [c]
(let [sc (.scale ^Currency (unit c))]
(when-not (= sc auto-scaled) sc)))
(^Integer [c ^Registry registry]
(let [sc (.scale ^Currency (unit c registry))]
(when-not (= sc auto-scaled) sc)))
(^Integer [c ^Registry locale registry]
(let [sc (.scale ^Currency (unit c registry))]
(when-not (= sc auto-scaled) sc))))
(def ^{:tag Integer
:arglists '(^Integer [c]
^Integer [c ^Registry registry]
^Integer [c locale ^Registry registry])}
scale
"Alias for sc."
sc)
(defn domain
"Returns currency domain as a keyword. For currencies with simple identifiers it will
be :ISO-4217. For currencies with namespace-qualified identifiers it will be the
upper-cased namespace name (e.g. :CRYPTO) set during creation a currency
object. Locale argument is ignored."
{:tag clojure.lang.Keyword :added "1.0.0"}
(^clojure.lang.Keyword [c] (.domain ^Currency (unit c)))
(^clojure.lang.Keyword [c ^Registry registry] (.domain ^Currency (unit c registry)))
(^clojure.lang.Keyword [c locale ^Registry registry] (.domain ^Currency (unit c registry))))
(def ^{:tag clojure.lang.Keyword
:arglists '(^clojure.lang.Keyword [c]
^clojure.lang.Keyword [c, ^Registry registry]
^clojure.lang.Keyword [c, locale ^Registry registry])}
ns
"Alias for domain."
domain)
(defn kind
"Returns currency kind. It is a keyword which describes origin of its value. Currently
known kinds are:
- :FIAT – legal tender issued by government or other authority
- :FIDUCIARY - accepted medium of exchange issued by a fiduciary or fiduciaries
- :DECENTRALIZED - accepted medium of exchange issued by a distributed ledger
- :COMBANK - commercial bank money
- :COMMODITY - accepted medium of exchange based on commodities
- :EXPERIMENTAL - pseudo-currency used for testing purposes.
The function may return nil if the currency is a no-currency. Locale argument is
ignored."
{:tag clojure.lang.Keyword :added "1.0.0"}
(^clojure.lang.Keyword [c] (.kind ^Currency (unit c)))
(^clojure.lang.Keyword [c ^Registry registry] (.kind ^Currency (unit c registry)))
(^clojure.lang.Keyword [c locale ^Registry registry] (.kind ^Currency (unit c registry))))
(defn ns-code
"Returns a currency code as a string for the given currency object. If the currency
identifier is namespaced the namespace will be used as a prefix and slash character
as a separator. Locale argument is ignored."
{:tag String :added "1.0.0"}
(^String [c] (subs (str (id c)) 1))
(^String [c ^Registry registry] (subs (str (id c registry)) 1))
(^String [c locale ^Registry registry] (subs (str (id c registry)) 1)))
(defn code
"Returns a currency code as a string for the given currency object. If the currency
identifier is namespaced only the base code (without a namespace) will be
returned. Locale argument is ignored."
{:tag String :added "1.0.0"}
(^String [c] (core-name (id c)))
(^String [c ^Registry registry] (core-name (id c registry)))
(^String [c locale ^Registry registry] (core-name (id c registry))))
(defn weight
"Returns weight of the given currency (used to resolve conflicts when getting
currencies having conflicting currency codes)."
{:tag 'int :added "1.0.2"}
([c] (int (.weight ^Currency (unit c))))
([c ^Registry registry] (int (.weight ^Currency (unit c registry))))
([c locale ^Registry registry] (int (.weight ^Currency (unit c registry)))))
;;
;; Currency - country relations.
;;
(defn countries
"Returns a set of country IDs (keywords) for which the given currency is main
currency. If there are no countries associated with a currency, returns nil. Locale
argument is ignored."
{:tag clojure.lang.PersistentHashSet :added "1.0.0"}
(^clojure.lang.PersistentHashSet [c]
(countries c (registry/get)))
(^clojure.lang.PersistentHashSet [c ^Registry registry]
(get (registry/currency-id->country-ids registry) (id c)))
(^clojure.lang.PersistentHashSet [c locale ^Registry registry]
(get (registry/currency-id->country-ids registry) (id c))))
(defn of-country
"Returns a currency for the given country identified by a country ID (which should be
a keyword). If there is no currency or country of the given ID does not exist,
returns nil. Locale argument is ignored."
{:tag Currency :added "1.0.0"}
(^Currency [^clojure.lang.Keyword country-id]
(of-country country-id (registry/get)))
(^Currency [^clojure.lang.Keyword country-id ^Registry registry]
(get (registry/country-id->currency registry) (keyword country-id)))
(^Currency [^clojure.lang.Keyword country-id locale ^Registry registry]
(get (registry/country-id->currency registry) (keyword country-id))))
;;
;; Parsing helpers.
;;
(defn prep-currency
"Prepares currency attributes which may come from an external data source. Returns a
currency."
{:tag Currency :added "1.0.0" :private true}
(^Currency [[id {:keys [numeric kind scale domain weight]}]]
(prep-currency id numeric kind scale domain weight))
(^Currency [id {:keys [numeric kind scale domain weight]}]
(prep-currency id numeric kind scale domain weight))
(^Currency [id numeric kind scale]
(prep-currency id numeric kind scale nil 0))
(^Currency [id numeric kind scale domain weight]
(when (some? id)
(let [numeric (if (number? numeric) numeric (or (try-parse-long numeric) no-numeric-id))
numeric (if (< numeric 1) no-numeric-id numeric)
scale (if (number? scale) scale (or (try-parse-int scale) auto-scaled))
scale (if (< scale 0) auto-scaled scale)
kind (when (some? kind) (keyword kind))
weight (if (number? weight) weight (try-parse-int weight))]
(new-currency (keyword id) (long numeric) (int scale) kind domain weight)))))
(defn prep-currencies
"Prepares a map of currency ID to currency based on a configuration map of currency
ID to currency attributes."
{:tag clojure.lang.IPersistentMap :added "1.0.0" :private true}
[^clojure.lang.IPersistentMap m]
(map prep-currency m))
(defn prep-cur->ctr
"Prepares countries map which may come from an external data source. Expects a map of
country ID to currency ID. Returns a map of currency ID to sets of country IDs."
{:tag clojure.lang.IPersistentMap :added "1.0.0" :private true}
[ctr-id->cur-id]
(->> ctr-id->cur-id
(map/remove-empty-values)
(map/map-keys-and-vals #(vector (keyword %1) (keyword %2)))
(map/invert-in-sets)
(map/remove-empty-values)))
(defn prep-country-ids
"Prepares country identifiers by converting the given object into a sequence of
keywords."
{:tag clojure.lang.LazySeq :added "1.0.0" :private true}
[country-ids]
(when country-ids
(let [cids (if (sequential? country-ids) country-ids
(if (and (seqable? country-ids) (not (string? country-ids)))
(seq country-ids)
(list country-ids)))
cids (if (set? cids) cids (distinct cids))]
(map keyword cids))))
(defn prep-localized-props
"Prepares localized properties map for a single currency."
{:tag clojure.lang.IPersistentMap :added "1.0.0" :private true}
[^clojure.lang.IPersistentMap p]
(map/map-keys-and-vals
#(vector (let [k (keyword %1)] (if (= :* k) k (l/locale k)))
(map/map-vals str %2)) p))
(defn prep-all-localized-props
"Prepares localized properties map for all currencies in a map."
{:tag clojure.lang.IPersistentMap :added "1.0.0" :private true}
[^clojure.lang.IPersistentMap p]
(map/map-vals prep-localized-props p))
(defn weighted-currencies
"Constructor for weighted currencies entry in :cur-code->currencies database of a
registry."
{:tag clojure.lang.PersistentTreeSet :private true :added "1.0.2"}
[^clojure.lang.PersistentTreeSet s]
(or s (sorted-set-by
(fn [^Currency a ^Currency b] (compare (:weight b) (:weight a))))))
;;
;; Adding and removing to/from registry.
;;
(defn remove-countries-core
"Removes countries from the given registry. Also unlinks constrained currencies in
proper locations. Returns updated registry."
{:tag Registry :private true :added "1.0.0"}
[^Registry registry country-ids]
(if-not (some? (seq country-ids))
registry
(let [ctr-to-cur (registry/country-id->currency registry)
cid-to-ctrs (registry/currency-id->country-ids registry)
currency-ids (map #(.id ^Currency %) (distinct (filter identity (map ctr-to-cur country-ids))))
new-cid-ctr (reduce #(apply core-update %1 %2 disj country-ids) cid-to-ctrs currency-ids)]
(-> registry
(assoc :cur-id->ctr-ids (map/remove-empty-values new-cid-ctr currency-ids))
(assoc :ctr-id->cur (apply dissoc ctr-to-cur country-ids))))))
(defn remove-currency-from-set
"Removed currency object from a set and returns nil if the set is empty after
this operation."
{:tag clojure.lang.PersistentTreeSet :private true :added "1.0.2"}
[^clojure.lang.PersistentTreeSet s ^Currency cur]
(when s
(if cur
(let [r (disj s cur)] (if (zero? (count r)) nil r))
s)))
(defn remove-weighted-currency
"Removes currency object from a sorted set associated with a currency code keyword in
a map."
{:tag clojure.lang.PersistentHashMap :private true :added "1.0.2"}
[^clojure.lang.PersistentHashMap m cur-code ^Currency cur]
(if-not (contains? m cur-code) m
(if-some [ncurs (remove-currency-from-set (get m cur-code) cur)]
(assoc m cur-code ncurs)
(dissoc m cur-code))))
(defn unregister
"Removes a currency from the given registry. Also removes country and numeric ID
constrains when necessary and all localized properties associated with a
currency. Returns updated registry.
Please note that removal of a currency whose identifier and numeric identifier are
the same as the currencies which are already registered, will not only remove the
existing currency identified by the ID but also remove numeric ID from within
all currency objects present in a registry."
{:tag Registry :added "1.0.0"}
[^Registry registry currency]
(when registry
(let [^Currency cur (if (instance? Currency currency) currency (of-id currency registry))
id (.id ^Currency cur)
cur-code (if (namespace id) (keyword (core-name id)) id)
proposed-nr (.numeric ^Currency cur)
proposed-nr (when (not= proposed-nr no-numeric-id) proposed-nr)
registered-id id
registered-cur (get (registry/currency-id->currency registry) registered-id)
registered-nr (when registered-cur (.numeric ^Currency registered-cur))
registered-nr (when (and registered-nr (not= registered-nr no-numeric-id)) registered-nr)
registered-by-nr (when proposed-nr (get (registry/currency-nr->currency registry) (long proposed-nr)))
registered-by-nr-id (when registered-by-nr (.id ^Currency registered-by-nr))
new-by-nr (when (and registered-by-nr-id
(or (not= registered-by-nr-id registered-id)
(not= registered-by-nr registered-nr)))
(assoc registered-by-nr :numeric (long no-numeric-id)))
country-ids (get (registry/currency-id->country-ids registry) registered-id)
^Registry registry (if-not new-by-nr
registry
(-> registry
(assoc-in [:cur-id->cur registered-by-nr-id] new-by-nr)
(assoc-in [:ctr-id->cur registered-by-nr-id] new-by-nr)
(map/dissoc-in [:cur-nr->cur proposed-nr])))
^Registry registry (-> registry
(map/dissoc-in [:cur-id->cur registered-id])
(map/dissoc-in [:cur-nr->cur registered-nr])
(map/dissoc-in [:cur-id->localized registered-id])
(core-update :cur-code->curs remove-weighted-currency cur-code registered-cur))]
(if-not (contains? (registry/currency-id->country-ids registry) registered-id)
registry
(as-> registry regi
(map/dissoc-in regi [:cur-id->ctr-ids registered-id])
(apply core-update regi :ctr-id->cur dissoc country-ids))))))
(defn remove-countries
"Removes countries from the given registry. Also unlinks constrained currencies in
proper locations. Returns updated registry."
{:tag Registry :added "1.0.0"}
[^Registry registry country-ids]
(when registry
(if (nil? country-ids)
registry
(remove-countries-core registry (prep-country-ids country-ids)))))
(defn add-countries
"Associates the given country or countries with a currency. If the currency does not
exist, exception is thrown. If the currency exists but differs in any detail from
the existing currency from the registry, exception is thrown. If the currency
exists and equals to the given in any aspect, country associations are added. Links
from other countries to the currency are not removed unless the country is already
linked with some other currency; in this case it will be unlinked first."
{:tag Registry :added "1.0.0"}
[^Registry registry ^Currency currency country-ids]
(when (some? registry)
(when-not (defined? currency registry)
(throw
(ex-info (str "Currency "
(if (instance? Currency currency) (.id ^Currency currency) currency)
" does not exist in a registry.") {:currency currency})))
(let [^Currency c (if (instance? Currency currency) currency (of-id currency registry))
cid (.id ^Currency c)
^Currency p (get (registry/currency-id->currency registry) cid)
cids (prep-country-ids country-ids)]
(when-not (= c p)
(throw
(ex-info (str "Currency " cid " differs from the currency existing in a registry.")
{:currency c, :existing-currency p})))
(if (nil? (seq cids)) registry
(as-> registry regi
(remove-countries-core regi cids)
(apply update-in regi [:cur-id->ctr-ids cid] (fnil conj #{}) (set cids))
(core-update regi :ctr-id->cur (partial apply assoc) (interleave cids (repeat c))))))))
(defn remove-localized-properties
"Removes localized properties assigned to a currency. Returns updated registry."
{:tag Registry :added "1.0.0"}
[^Registry registry ^Currency currency]
(when registry
(if (nil? currency)
registry
(map/dissoc-in registry [:cur-id->localized (.id ^Currency currency)]))))
(defn add-localized-properties
"Adds localized properties of a currency to the given registry. Overwrites existing
properties."
{:tag Registry :added "1.0.0"}
[^Registry registry ^Currency currency properties]
(when (some? registry)
(when-not (defined? currency registry)
(throw
(ex-info (str "Currency "
(if (instance? Currency currency) (.id ^Currency currency) currency)
" does not exist in a registry.") {:currency currency})))
(let [^Currency c (if (instance? Currency currency) currency (of-id currency registry))
cid (.id ^Currency c)
^Currency p (get (registry/currency-id->currency registry) cid)]
(when-not (= c p)
(throw
(ex-info (str "Currency " cid " differs from the currency existing in a registry.")
{:currency c, :existing-currency p})))
(if (and (map? properties) (pos? (count properties)))
(assoc-in registry [:cur-id->localized cid] (prep-localized-props properties))
registry))))
(defn add-weighted-currency
"Adds currency code to the given registry using .weight field of a currency. Currency
must exist in a cur-id->cur database of the registry as it will be the source
object when adding to cur-code->curs database. The registry will not be updated if
the given weight is lower than the existing. If it is the same, exception will be
thrown."
{:tag Registry :added "1.0.2"}
[^Registry registry ^Currency currency]
(when (some? registry)
(when-not (defined? currency registry)
(throw
(ex-info (str "Currency "
(if (instance? Currency currency) (.id ^Currency currency) currency)
" does not exist in a registry.") {:currency currency})))
(let [^Currency c (if (instance? Currency currency) currency (of-id currency registry))
cid (.id ^Currency c)
^Currency p (of-id cid registry)
p-weight (int (.weight ^Currency p))
kw-code (if (simple-keyword? cid) cid (keyword (core-name cid)))
currencies (get (registry/currency-code->currencies registry) kw-code)
same-code (first (drop-while #(not= p-weight (.weight ^Currency %)) currencies))]
(when same-code
(throw (ex-info "Currency code with the same weight already exists."
{:existing-currency same-code :currency c :registry registry})))
(update-in registry [:cur-code->curs kw-code] #(conj (weighted-currencies %) p)))))
(defn register
"Adds a currency and optional, associated country mappings and/or localized
properties to the given registry. Returns updated registry.
The optional country-ids argument should be a sequence of keywords (however, if a
single keyword is given it will be converted to a single-element sequence) with
country codes which should be associated with the given currency.
The optional localized-properties argument should be a map of localized properties
of the given currency. See the Data Structures documentation section for more
information.
If the update mode is enabled then all of the current countries associated with the
currency are removed and replaced with the provided ones. To simply add new
countries, use add-countries. Also note that update mode removed localized
properties so new one must be provided."
{:tag Registry :added "1.0.0"
:arglists '(^Registry [^Registry registry currency]
^Registry [^Registry registry currency country-ids]
^Registry [^Registry registry currency update-mode?]
^Registry [^Registry registry currency country-ids update-mode?]
^Registry [^Registry registry currency country-ids localized-properties]
^Registry [^Registry registry currency country-ids localized-properties update-mode?])}
(^Registry [^Registry registry currency]
(register registry currency nil nil false))
(^Registry [^Registry registry currency country-ids-or-update?]
(if (boolean? country-ids-or-update?)
(register registry currency nil nil country-ids-or-update?)
(register registry currency country-ids-or-update? nil false)))
(^Registry [^Registry registry currency country-ids localized-or-update]
(if (boolean? localized-or-update)
(register registry currency country-ids nil localized-or-update)
(register registry currency country-ids localized-or-update false)))
(^Registry [^Registry registry currency country-ids localized-properties ^Boolean update?]
(when (some? registry)
(let [^Currency c (if (instance? Currency currency) currency (of-id currency registry))
cid (.id ^Currency c)
cnr (.numeric ^Currency c)
cid-to-cur (registry/currency-id->currency registry)
cnr-to-cur (registry/currency-nr->currency registry)]
(when-not update?
(when-some [^Currency p (get cid-to-cur cid)]
(throw (ex-info
(str "Currency " cid " already exists in a registry.")
{:currency c, :existing-currency p})))
(when-some [^Currency p (get cnr-to-cur cnr)]
(throw (ex-info
(str "Currency with numeric ID of " cnr " already exists in a registry.")
{:currency c, :existing-currency p}))))
(let [registry (unregister registry c)
cid-to-cur (registry/currency-id->currency registry)
registry (assoc registry :cur-id->cur (assoc cid-to-cur cid c))
numeric-id (.numeric ^Currency c)
cnr-to-cur (registry/currency-nr->currency ^Registry registry)
registry (if (or (nil? numeric-id) (= numeric-id no-numeric-id) (<= numeric-id 0)) registry
(assoc registry :cur-nr->cur (assoc cnr-to-cur (long numeric-id) c)))]
(-> registry
(add-weighted-currency currency)
(add-countries currency country-ids)
(add-localized-properties currency localized-properties)))))))
(declare localized-properties)
(defn update
"Replaces a currency in the given registry by a new one, preserving localized
properties, relation to countries and code if not explicitly given. Returns updated
registry. If the currency does not exist in a registry yet, it will be registered."
{:tag Registry :added "1.1.0"}
([^Registry registry currency]
(update registry currency nil nil))
([^Registry registry currency country-ids]
(update registry currency country-ids nil))
([^Registry registry currency country-ids localized-properties-map]
(let [present (if (instance? Currency currency) currency (unit currency registry))]
(register registry
present
(or country-ids (countries present))
(or localized-properties-map (localized-properties present))
true))))
(defn update!
"Replaces a currency in the global, shared registry by a new one, preserving
localized properties, relation to countries and code if not explicitly
given. Returns updated registry. If the currency does not exist in a registry yet,
it will be registered."
{:tag Registry :added "1.1.0"}
([currency]
(swap! registry/R update currency nil nil))
([currency country-ids]
(swap! registry/R update currency country-ids nil))
([currency country-ids localized-properties-map]
(swap! registry/R update currency country-ids localized-properties-map)))
(defn register!
"Adds currency and (optional) country to the global registry. Returns updated
registry. When the currency is nil, returns current state of the global, shared