|
1 | 1 | {-# LANGUAGE DerivingStrategies #-} |
2 | 2 | {-# LANGUAGE FunctionalDependencies #-} |
3 | 3 | {-# LANGUAGE TypeFamilyDependencies #-} |
| 4 | +{-# LANGUAGE UndecidableInstances #-} |
4 | 5 |
|
5 | 6 | module Money.Theory.SemanticMoney where |
6 | 7 |
|
7 | 8 | import Data.Default (Default (..)) |
8 | 9 | import Data.Kind (Type) |
| 10 | +import Data.List |
9 | 11 |
|
10 | 12 |
|
11 | 13 | -- | Type system trite: types used in semantic money |
@@ -238,3 +240,97 @@ instance ( MonetaryTypes mt, t ~ MT_TIME mt, v ~ MT_VALUE mt, u ~ MT_UNIT mt, f |
238 | 240 | (r', er') = if u' == 0 then (0, r `mt_fr_mul_u` u) else r `mt_fr_mul_u_qr_u` (u, u') |
239 | 241 | b' = fst . flow1 r' $ b |
240 | 242 | a' = fst . flow1 (er' + flowRate a) $ a |
| 243 | + |
| 244 | +-- Represents a single snapshot of change |
| 245 | +data MoneyEvent mt = MoneyEvent |
| 246 | + { eventTime :: MT_TIME mt |
| 247 | + , eventValueDelta :: MT_VALUE mt -- Discrete value change |
| 248 | + , eventFlowDelta :: MT_FLOWRATE mt -- Rate change |
| 249 | + , eventIndicator :: Int -- For conflict resolution |
| 250 | + } |
| 251 | + |
| 252 | +-- Representation of an accuont |
| 253 | +data AccountState mt = AccountState |
| 254 | + { asTime :: MT_TIME mt |
| 255 | + , asValue :: MT_VALUE mt |
| 256 | + , asFlowRate :: MT_FLOWRATE mt |
| 257 | + } |
| 258 | + |
| 259 | +-- Make the types be Eq compatible |
| 260 | +instance ( MonetaryTypes mt |
| 261 | + , Eq (MT_TIME mt) |
| 262 | + , Eq (MT_VALUE mt) |
| 263 | + , Eq (MT_FLOWRATE mt) |
| 264 | + ) => Eq (MoneyEvent mt) where |
| 265 | + a == b = eventTime a == eventTime b |
| 266 | + && eventValueDelta a == eventValueDelta b |
| 267 | + && eventFlowDelta a == eventFlowDelta b |
| 268 | + && eventIndicator a == eventIndicator b |
| 269 | + |
| 270 | + (/=) a b = not (a == b) |
| 271 | + |
| 272 | +instance ( MonetaryTypes mt |
| 273 | + , Eq (MT_TIME mt) |
| 274 | + , Eq (MT_VALUE mt) |
| 275 | + , Eq (MT_FLOWRATE mt) |
| 276 | + ) => Eq (AccountState mt) where |
| 277 | + a == b = asTime a == asTime b |
| 278 | + && asValue a == asValue b |
| 279 | + && asFlowRate a == asFlowRate b |
| 280 | + |
| 281 | + (/=) a b = not (a == b) |
| 282 | + |
| 283 | +-- Takes two AccountState and provides their delta as a MoneyEvent |
| 284 | +deltaEvents :: MonetaryTypes mt |
| 285 | + => AccountState mt -- Previous state |
| 286 | + -> AccountState mt -- Current state |
| 287 | + -> Maybe (MoneyEvent mt) |
| 288 | +deltaEvents prev current |
| 289 | + | asTime prev >= asTime current = Nothing |
| 290 | + | otherwise = Just $ MoneyEvent |
| 291 | + { eventTime = asTime current |
| 292 | + , eventValueDelta = asValue current - (asValue prev + valueGrowth) |
| 293 | + , eventFlowDelta = asFlowRate current - asFlowRate prev |
| 294 | + , eventIndicator = 0 |
| 295 | + } |
| 296 | + where |
| 297 | + deltaT = asTime current - asTime prev |
| 298 | + valueGrowth = asFlowRate prev `mt_fr_mul_t` fromIntegral deltaT |
| 299 | + |
| 300 | +-- Ensure that the snapshots are conflict-free |
| 301 | +ensureSnapshots :: (MonetaryTypes mt, Ord (MT_TIME mt)) |
| 302 | + => [MoneyEvent mt] |
| 303 | + -> Either String [MoneyEvent mt] |
| 304 | +ensureSnapshots snaps = do |
| 305 | + let sorted = sortOn eventTime snaps |
| 306 | + validated <- validateTimeline sorted |
| 307 | + pure $ settleSnapshots validated |
| 308 | + |
| 309 | +-- These two functions below are not required as of now, but they are |
| 310 | +-- useful for any real usage of MoneyEvents, since the logic is to |
| 311 | +-- have an ordering and operate on the snapshots |
| 312 | + |
| 313 | +-- Ensure that the set of snapshots is strictly ordered by timeline |
| 314 | +validateTimeline :: (MonetaryTypes mt, Ord (MT_TIME mt)) |
| 315 | + => [MoneyEvent mt] |
| 316 | + -> Either String [MoneyEvent mt] |
| 317 | +validateTimeline [] = Right [] |
| 318 | +validateTimeline [x] = Right [x] |
| 319 | +validateTimeline (x:y:xs) |
| 320 | + | eventTime x > eventTime y = Left "Snapshots out of temporal order" |
| 321 | + | eventTime x == eventTime y && eventIndicator x >= eventIndicator y = |
| 322 | + Left "Conflicting snapshots: same timestamp with non-decreasing nonce" |
| 323 | + | otherwise = (x:) <$> validateTimeline (y:xs) |
| 324 | + |
| 325 | +-- Merge and settle changes of snapshots that are in the same timeline |
| 326 | +settleSnapshots :: MonetaryTypes mt => [MoneyEvent mt] -> [MoneyEvent mt] |
| 327 | +settleSnapshots = map mergeGroup . groupBy sameTime |
| 328 | + where |
| 329 | + sameTime a b = eventTime a == eventTime b |
| 330 | + mergeGroup [] = error "Empty group" |
| 331 | + mergeGroup grp@(x:_) = MoneyEvent |
| 332 | + { eventTime = eventTime x |
| 333 | + , eventValueDelta = sum (map eventValueDelta grp) |
| 334 | + , eventFlowDelta = sum (map eventFlowDelta grp) |
| 335 | + , eventIndicator = maximum (map eventIndicator grp) |
| 336 | + } |
0 commit comments