Skip to content

Commit 3cb01cd

Browse files
safareligaryb
authored andcommitted
Add Interval (#52)
* WIP: Add Interval * remove unused imports * remove dublicats * update Duration * add couple instances for Duration and Interval * reorder code * remove mkDuration DurationView and Milliseconds component * fis ord instance of DurationComponent * make Interval Bifunctor * add Bifoldable for Interval * add IsoDuration; use Map instead of List Tuple * derive instances for RecurringInterval * fix derivations * add Bitraversable and Eq instances * export Duration and DurationComponent * make sure IsoDuraiton is not empty * add ^ to PS version * revert travis changes * make Duration new type and derive Newype * make duration components singular * reverse duration component order * make isoDuration newtype * allow only positive values in duration * remove some TODOs * fix spaces and unicodes * add ord instances * fix spacing, parens and $ usage * rename JustDuration to DurationOnly * split Interval - move Interval.purs up one level - move Duration and IsoDuration parts away * reverse DurationComponent order * add Week component to Duration; refactor Duration.Iso * move checkWeekUsage up add tests This way InvalidWeekComponentUsage will be first if it is present in errors * use NonEmptyList instead of NonEmpty Array; add prettyError
1 parent 0373fa8 commit 3cb01cd

File tree

7 files changed

+320
-16
lines changed

7 files changed

+320
-16
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ script:
1515
- bower install --production
1616
- npm run -s build
1717
- bower install
18-
- npm test
18+
- npm run -s test
1919
after_success:
2020
- >-
2121
test $TRAVIS_TAG &&

bower.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
"purescript-functions": "^3.0.0",
2121
"purescript-generics": "^4.0.0",
2222
"purescript-integers": "^3.0.0",
23-
"purescript-math": "^2.0.0",
24-
"purescript-foldable-traversable": "^3.0.0"
23+
"purescript-foldable-traversable": "^3.0.0",
24+
"purescript-maps": "^3.0.0",
25+
"purescript-math": "^2.0.0"
2526
},
2627
"devDependencies": {
2728
"purescript-assert": "^3.0.0",

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
},
88
"devDependencies": {
99
"eslint": "^3.17.1",
10-
"pulp": "^10.0.4",
11-
"purescript-psa": "^0.5.0-rc.1",
10+
"pulp": "^11.0.x",
11+
"purescript-psa": "^0.5.x",
1212
"rimraf": "^2.6.1"
1313
}
1414
}

src/Data/Interval.purs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
module Data.Interval
2+
( Interval(..)
3+
, RecurringInterval(..)
4+
, module DurationExports
5+
) where
6+
7+
import Prelude
8+
9+
import Control.Extend (class Extend, extend)
10+
import Data.Bifoldable (class Bifoldable, bifoldl, bifoldr, bifoldrDefault, bifoldMapDefaultL)
11+
import Data.Bifunctor (class Bifunctor, bimap)
12+
import Data.Bitraversable (class Bitraversable, bitraverse, bisequenceDefault)
13+
import Data.Foldable (class Foldable, foldl, foldr, foldrDefault, foldMapDefaultL)
14+
import Data.Interval.Duration as DurationExports
15+
import Data.Maybe (Maybe)
16+
import Data.Traversable (class Traversable, traverse, sequenceDefault)
17+
18+
data RecurringInterval d a = RecurringInterval (Maybe Int) (Interval d a)
19+
20+
derive instance eqRecurringInterval :: (Eq d, Eq a) => Eq (RecurringInterval d a)
21+
derive instance ordRecurringInterval :: (Ord d, Ord a) => Ord (RecurringInterval d a)
22+
instance showRecurringInterval :: (Show d, Show a) => Show (RecurringInterval d a) where
23+
show (RecurringInterval x y) = "(RecurringInterval " <> show x <> " " <> show y <> ")"
24+
25+
interval :: d a. RecurringInterval d a -> Interval d a
26+
interval (RecurringInterval _ i) = i
27+
28+
over :: f d a d' a'. Functor f => (Interval d a -> f (Interval d' a')) -> RecurringInterval d a -> f (RecurringInterval d' a')
29+
over f (RecurringInterval n i) = map (RecurringInterval n) (f i)
30+
31+
instance functorRecurringInterval :: Functor (RecurringInterval d) where
32+
map f (RecurringInterval n i) = RecurringInterval n (map f i)
33+
34+
instance bifunctorRecurringInterval :: Bifunctor RecurringInterval where
35+
bimap f g (RecurringInterval n i) = RecurringInterval n (bimap f g i)
36+
37+
instance foldableRecurringInterval :: Foldable (RecurringInterval d) where
38+
foldl f i = foldl f i <<< interval
39+
foldr f i = foldr f i <<< interval
40+
foldMap = foldMapDefaultL
41+
42+
instance bifoldableRecurringInterval :: Bifoldable RecurringInterval where
43+
bifoldl f g i = bifoldl f g i <<< interval
44+
bifoldr f g i = bifoldr f g i <<< interval
45+
bifoldMap = bifoldMapDefaultL
46+
47+
instance traversableRecurringInterval :: Traversable (RecurringInterval d) where
48+
traverse f i = traverse f `over` i
49+
sequence = sequenceDefault
50+
51+
instance bitraversableRecurringInterval :: Bitraversable RecurringInterval where
52+
bitraverse l r i = bitraverse l r `over` i
53+
bisequence = bisequenceDefault
54+
55+
instance extendRecurringInterval :: Extend (RecurringInterval d) where
56+
extend f a@(RecurringInterval n i) = RecurringInterval n (extend (const (f a)) i)
57+
58+
data Interval d a
59+
= StartEnd a a
60+
| DurationEnd d a
61+
| StartDuration a d
62+
| DurationOnly d
63+
64+
derive instance eqInterval :: (Eq d, Eq a) => Eq (Interval d a)
65+
derive instance ordInterval :: (Ord d, Ord a) => Ord (Interval d a)
66+
instance showInterval :: (Show d, Show a) => Show (Interval d a) where
67+
show (StartEnd x y) = "(StartEnd " <> show x <> " " <> show y <> ")"
68+
show (DurationEnd d x) = "(DurationEnd " <> show d <> " " <> show x <> ")"
69+
show (StartDuration x d) = "(StartDuration " <> show x <> " " <> show d <> ")"
70+
show (DurationOnly d) = "(DurationOnly " <> show d <> ")"
71+
72+
instance functorInterval :: Functor (Interval d) where
73+
map = bimap id
74+
75+
instance bifunctorInterval :: Bifunctor Interval where
76+
bimap _ f (StartEnd x y) = StartEnd (f x) (f y)
77+
bimap g f (DurationEnd d x) = DurationEnd (g d) (f x)
78+
bimap g f (StartDuration x d) = StartDuration (f x) (g d)
79+
bimap g _ (DurationOnly d) = DurationOnly (g d)
80+
81+
instance foldableInterval :: Foldable (Interval d) where
82+
foldl f z (StartEnd x y) = (z `f` x) `f` y
83+
foldl f z (DurationEnd d x) = z `f` x
84+
foldl f z (StartDuration x d) = z `f` x
85+
foldl _ z _ = z
86+
foldr x = foldrDefault x
87+
foldMap = foldMapDefaultL
88+
89+
instance bifoldableInterval :: Bifoldable Interval where
90+
bifoldl _ f z (StartEnd x y) = (z `f` x) `f` y
91+
bifoldl g f z (DurationEnd d x) = (z `g` d) `f` x
92+
bifoldl g f z (StartDuration x d) = (z `g` d) `f` x
93+
bifoldl g _ z (DurationOnly d) = z `g` d
94+
bifoldr x = bifoldrDefault x
95+
bifoldMap = bifoldMapDefaultL
96+
97+
instance traversableInterval :: Traversable (Interval d) where
98+
traverse f (StartEnd x y) = StartEnd <$> f x <*> f y
99+
traverse f (DurationEnd d x) = f x <#> DurationEnd d
100+
traverse f (StartDuration x d) = f x <#> (_ `StartDuration` d)
101+
traverse _ (DurationOnly d) = pure (DurationOnly d)
102+
sequence = sequenceDefault
103+
104+
instance bitraversableInterval :: Bitraversable Interval where
105+
bitraverse _ r (StartEnd x y) = StartEnd <$> r x <*> r y
106+
bitraverse l r (DurationEnd d x) = DurationEnd <$> l d <*> r x
107+
bitraverse l r (StartDuration x d) = StartDuration <$> r x <*> l d
108+
bitraverse l _ (DurationOnly d) = DurationOnly <$> l d
109+
bisequence = bisequenceDefault
110+
111+
instance extendInterval :: Extend (Interval d) where
112+
extend f a@(StartEnd x y) = StartEnd (f a) (f a)
113+
extend f a@(DurationEnd d x) = DurationEnd d (f a)
114+
extend f a@(StartDuration x d) = StartDuration (f a) d
115+
extend f (DurationOnly d) = DurationOnly d

src/Data/Interval/Duration.purs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
module Data.Interval.Duration
2+
( Duration(..)
3+
, DurationComponent(..)
4+
, year
5+
, month
6+
, week
7+
, day
8+
, hour
9+
, minute
10+
, second
11+
, millisecond
12+
) where
13+
14+
import Prelude
15+
16+
import Data.Map as Map
17+
import Data.Monoid (class Monoid, mempty)
18+
import Data.Newtype (class Newtype)
19+
20+
newtype Duration = Duration (Map.Map DurationComponent Number)
21+
22+
derive instance eqDuration :: Eq Duration
23+
derive instance ordDuration :: Ord Duration
24+
derive instance newtypeDuration :: Newtype Duration _
25+
26+
instance showDuration :: Show Duration where
27+
show (Duration d) = "(Duration " <> show d <> ")"
28+
29+
instance semigroupDuration :: Semigroup Duration where
30+
append (Duration a) (Duration b) = Duration (Map.unionWith (+) a b)
31+
32+
instance monoidDuration :: Monoid Duration where
33+
mempty = Duration mempty
34+
35+
data DurationComponent = Second | Minute | Hour | Day | Week | Month | Year
36+
derive instance eqDurationComponent :: Eq DurationComponent
37+
derive instance ordDurationComponent :: Ord DurationComponent
38+
39+
instance showDurationComponent :: Show DurationComponent where
40+
show Minute = "Minute"
41+
show Second = "Second"
42+
show Hour = "Hour"
43+
show Day = "Day"
44+
show Week = "Week"
45+
show Month = "Month"
46+
show Year = "Year"
47+
48+
49+
week :: Number -> Duration
50+
week = durationFromComponent Week
51+
52+
year :: Number -> Duration
53+
year = durationFromComponent Year
54+
55+
month :: Number -> Duration
56+
month = durationFromComponent Month
57+
58+
day :: Number -> Duration
59+
day = durationFromComponent Day
60+
61+
hour :: Number -> Duration
62+
hour = durationFromComponent Hour
63+
64+
minute :: Number -> Duration
65+
minute = durationFromComponent Minute
66+
67+
second :: Number -> Duration
68+
second = durationFromComponent Second
69+
70+
millisecond :: Number -> Duration
71+
millisecond = durationFromComponent Second <<< (_ / 1000.0)
72+
73+
durationFromComponent :: DurationComponent -> Number -> Duration
74+
durationFromComponent k v = Duration (Map.singleton k v)

src/Data/Interval/Duration/Iso.purs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
module Data.Interval.Duration.Iso
2+
( IsoDuration
3+
, unIsoDuration
4+
, mkIsoDuration
5+
, Error(..)
6+
, Errors
7+
, prettyError
8+
) where
9+
10+
import Prelude
11+
12+
import Control.Plus (empty)
13+
import Data.Either (Either(..))
14+
import Data.Foldable (fold, foldMap)
15+
import Data.Interval.Duration (Duration(..), DurationComponent(..))
16+
import Data.List (List(..), reverse, span, null)
17+
import Data.List.NonEmpty (fromList)
18+
import Data.List.Types (NonEmptyList)
19+
import Data.Map as Map
20+
import Data.Maybe (Maybe(..), isJust)
21+
import Data.Monoid.Additive (Additive(..))
22+
import Data.Newtype (unwrap)
23+
import Data.Tuple (Tuple(..), snd)
24+
import Math as Math
25+
26+
newtype IsoDuration = IsoDuration Duration
27+
28+
derive instance eqIsoDuration :: Eq IsoDuration
29+
derive instance ordIsoDuration :: Ord IsoDuration
30+
instance showIsoDuration :: Show IsoDuration where
31+
show (IsoDuration d) = "(IsoDuration " <> show d <> ")"
32+
33+
type Errors = NonEmptyList Error
34+
35+
data Error
36+
= IsEmpty
37+
| InvalidWeekComponentUsage
38+
| ContainsNegativeValue DurationComponent
39+
| InvalidFractionalUse DurationComponent
40+
41+
derive instance eqError :: Eq Error
42+
derive instance ordError :: Ord Error
43+
instance showError :: Show Error where
44+
show (IsEmpty) = "(IsEmpty)"
45+
show (InvalidWeekComponentUsage) = "(InvalidWeekComponentUsage)"
46+
show (ContainsNegativeValue c) = "(ContainsNegativeValue " <> show c <> ")"
47+
show (InvalidFractionalUse c) = "(InvalidFractionalUse " <> show c <> ")"
48+
49+
prettyError :: Error -> String
50+
prettyError (IsEmpty) = "Duration is empty (has no components)"
51+
prettyError (InvalidWeekComponentUsage) = "Week component of Duration is used with other components"
52+
prettyError (ContainsNegativeValue c) = "Component `" <> show c <> "` contains negative value"
53+
prettyError (InvalidFractionalUse c) = "Invalid usage of Fractional value at component `" <> show c <> "`"
54+
55+
56+
unIsoDuration :: IsoDuration -> Duration
57+
unIsoDuration (IsoDuration a) = a
58+
59+
mkIsoDuration :: Duration -> Either Errors IsoDuration
60+
mkIsoDuration d = case fromList (checkValidIsoDuration d) of
61+
Just errs -> Left errs
62+
Nothing -> Right (IsoDuration d)
63+
64+
checkValidIsoDuration :: Duration -> List Error
65+
checkValidIsoDuration (Duration asMap) = check {asList, asMap}
66+
where
67+
asList = reverse (Map.toAscUnfoldable asMap)
68+
check = fold
69+
[ checkWeekUsage
70+
, checkEmptiness
71+
, checkFractionalUse
72+
, checkNegativeValues
73+
]
74+
75+
76+
type CheckEnv =
77+
{ asList :: List (Tuple DurationComponent Number)
78+
, asMap :: Map.Map DurationComponent Number}
79+
80+
checkWeekUsage :: CheckEnv -> List Error
81+
checkWeekUsage {asMap} = if isJust (Map.lookup Week asMap) && Map.size asMap > 1
82+
then pure InvalidWeekComponentUsage else empty
83+
84+
checkEmptiness :: CheckEnv -> List Error
85+
checkEmptiness {asList} = if null asList then pure IsEmpty else empty
86+
87+
checkFractionalUse :: CheckEnv -> List Error
88+
checkFractionalUse {asList} = case _.rest (span (snd >>> not isFractional) asList) of
89+
Cons (Tuple c _) rest | checkRest rest -> pure (InvalidFractionalUse c)
90+
_ -> empty
91+
where
92+
isFractional a = Math.floor a /= a
93+
checkRest rest = unwrap (foldMap (snd >>> Math.abs >>> Additive) rest) > 0.0
94+
95+
checkNegativeValues :: CheckEnv -> List Error
96+
checkNegativeValues {asList} = flip foldMap asList \(Tuple c num) ->
97+
if num >= 0.0 then empty else pure (ContainsNegativeValue c)

test/Test/Main.purs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,49 @@ import Prelude
44

55
import Control.Monad.Eff (Eff)
66
import Control.Monad.Eff.Console (CONSOLE, log)
7-
8-
import Data.Enum (class BoundedEnum, Cardinality, toEnum, enumFromTo, cardinality, succ, fromEnum, pred)
9-
import Data.Date as Date
10-
import Data.Time as Time
11-
import Data.Time.Duration as Duration
127
import Data.Array as Array
8+
import Data.Date as Date
139
import Data.DateTime as DateTime
14-
import Data.DateTime.Locale as Locale
1510
import Data.DateTime.Instant as Instant
11+
import Data.DateTime.Locale as Locale
12+
import Data.Either (Either(..), isRight)
13+
import Data.Enum (class BoundedEnum, Cardinality, toEnum, enumFromTo, cardinality, succ, fromEnum, pred)
1614
import Data.Foldable (foldl, foldr, foldMap)
15+
import Data.Interval as Interval
16+
import Data.Interval.Duration.Iso as IsoDuration
1717
import Data.Maybe (Maybe(..), fromJust)
18+
import Data.Monoid (mempty)
19+
import Data.Newtype (over, unwrap)
1820
import Data.String (length)
21+
import Data.Time as Time
22+
import Data.Time.Duration as Duration
1923
import Data.Traversable (sequence, traverse)
2024
import Data.Tuple (Tuple(..), snd)
21-
import Data.Newtype (over, unwrap)
22-
2325
import Math (floor)
24-
25-
import Type.Proxy (Proxy(..))
26-
import Test.Assert (ASSERT, assert)
2726
import Partial.Unsafe (unsafePartial)
27+
import Test.Assert (ASSERT, assert)
28+
import Type.Proxy (Proxy(..))
2829

2930
type Tests = Eff (console :: CONSOLE, assert :: ASSERT) Unit
3031

3132
main :: Tests
3233
main = do
34+
log "check Duration monoid"
35+
assert $ Interval.year 1.0 == mempty <> Interval.year 2.0 <> Interval.year 1.0 <> Interval.year (-2.0)
36+
assert $ Interval.second 0.5 == Interval.millisecond 500.0
37+
assert $ IsoDuration.mkIsoDuration (Interval.week 1.2 <> Interval.week 1.2)
38+
== IsoDuration.mkIsoDuration (Interval.week 2.4)
39+
assert $ isRight $ IsoDuration.mkIsoDuration (Interval.day 1.2 <> mempty)
40+
assert $ isRight $ IsoDuration.mkIsoDuration (Interval.day 1.2 <> Interval.second 0.0)
41+
assert $ isRight $ IsoDuration.mkIsoDuration (Interval.year 2.0 <> Interval.day 1.0)
42+
assert $ IsoDuration.mkIsoDuration (Interval.year 2.5 <> Interval.day 1.0)
43+
== Left (pure (IsoDuration.InvalidFractionalUse Interval.Year))
44+
log $ show $ IsoDuration.mkIsoDuration (Interval.year 2.5 <> Interval.week 1.0)
45+
== Left (pure IsoDuration.InvalidWeekComponentUsage <> pure (IsoDuration.InvalidFractionalUse Interval.Year))
46+
assert $ IsoDuration.mkIsoDuration (Interval.year 2.0 <> Interval.day (-1.0))
47+
== Left (pure (IsoDuration.ContainsNegativeValue Interval.Day))
48+
assert $ IsoDuration.mkIsoDuration (mempty)
49+
== Left (pure IsoDuration.IsEmpty)
3350

3451
let epochDate = unsafePartial fromJust $ Date.canonicalDate
3552
<$> toEnum 1

0 commit comments

Comments
 (0)