monadic 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +8 -0
- data/CHANGELOG.md +14 -0
- data/README.md +41 -44
- data/Rakefile +9 -0
- data/lib/monadic/either.rb +86 -71
- data/lib/monadic/maybe.rb +92 -0
- data/lib/monadic/monad.rb +38 -0
- data/lib/monadic/validation.rb +22 -18
- data/lib/monadic/version.rb +1 -1
- data/lib/monadic.rb +2 -3
- data/monadic.gemspec +2 -1
- data/spec/either_spec.rb +26 -6
- data/spec/jruby_fixes.rb +3 -0
- data/spec/maybe_spec.rb +145 -0
- data/spec/monad_axioms.rb +37 -0
- data/spec/monad_spec.rb +12 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/validation_spec.rb +1 -1
- metadata +31 -6
- data/lib/monadic/option.rb +0 -101
- data/spec/option_spec.rb +0 -129
data/.travis.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.0.6
|
4
|
+
**Contains Breaking Changes**
|
5
|
+
|
6
|
+
Refactoring to use internal `Monad` class, from which all monads inherit.
|
7
|
+
|
8
|
+
Reimplemented `Maybe` as inherited class from `Monad`. The old `Option` implementation has been removed, maybe does the same though, but the code is much cleaner and obeys the 3 monadic laws.
|
9
|
+
|
10
|
+
Removed `Maybe#fetch` call with block`
|
11
|
+
|
12
|
+
`Either` and `Validation` are now in the `Monadic` namespace.
|
13
|
+
|
14
|
+
Added Travis-Ci integration, [](http://travis-ci.org/pzol/monadic)
|
15
|
+
|
16
|
+
|
3
17
|
## v0.0.5
|
4
18
|
|
5
19
|
Removed the `#chain` method alias for bind in `Either`.
|
data/README.md
CHANGED
@@ -1,25 +1,23 @@
|
|
1
1
|
# Monadic
|
2
2
|
|
3
|
-
helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in [Scala](http://www.scala-lang.org/)
|
3
|
+
helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in [Scala](http://www.scala-lang.org/) and [Haskell](http://www.haskell.org/) to my ruby projects.
|
4
4
|
|
5
5
|
My motivation to create this gem was that I often work with nested Hashes and need to reach deeply inside of them so my code is sprinkled with things like some_hash.fetch(:one, {}).fetch(:two, {}).fetch(:three, "unknown").
|
6
6
|
|
7
7
|
We have the following monadics (monads, functors, applicatives and variations):
|
8
8
|
|
9
|
-
-
|
10
|
-
- Either -
|
11
|
-
- Validation
|
9
|
+
- Maybe - use if you have __one__ exception
|
10
|
+
- Either - use if you have __many__ exceptions, and one call depends on the previous
|
11
|
+
- Validation - use if you have __many__ independent calls (usually to validate an object)
|
12
12
|
|
13
13
|
What's the point of using monads in ruby? To me it started with having a safe way to deal with nil objects and other exceptions.
|
14
|
-
Thus you contain the erroneous behaviour within a monad - an indivisible, impenetrable unit.
|
15
|
-
|
16
|
-
Monad purists might complain that there is no unit method to get the zero monad, I didn't include them, as I didn't find this idiomatic to the ruby language. I prefer to focus on the pragmatic uses of monads. If you want to learn moar about monads, see the references section at the bottom.
|
17
|
-
|
14
|
+
Thus you contain the erroneous behaviour within a monad - an indivisible, impenetrable unit. Functional programming considers _throwing_ exceptions to be a side-effect, instead we _propagate_ exceptions, i.e. return them as a result of a function call.
|
15
|
+
|
18
16
|
A monad is most effectively described as a computation that eventually returns a value. -- Wolfgang De Meuter
|
19
17
|
|
20
18
|
## Usage
|
21
19
|
|
22
|
-
###
|
20
|
+
### Maybe
|
23
21
|
Is an optional type, which helps to handle error conditions gracefully. The one thing to remember about option is: 'What goes into the Option, stays in the Option'.
|
24
22
|
|
25
23
|
Option(User.find(123)).name._ # ._ is a shortcut for .fetch
|
@@ -28,57 +26,51 @@ Is an optional type, which helps to handle error conditions gracefully. The one
|
|
28
26
|
Maybe(User.find(123)).name._
|
29
27
|
|
30
28
|
# confidently diving into nested hashes
|
31
|
-
Maybe({})[:a][:b][:c] ==
|
32
|
-
Maybe({})[:a][:b][:c].fetch('unknown') ==
|
29
|
+
Maybe({})[:a][:b][:c] == Nothing
|
30
|
+
Maybe({})[:a][:b][:c].fetch('unknown') == "unknown"
|
33
31
|
Maybe(a: 1)[:a]._ == 1
|
34
32
|
|
35
33
|
Basic usage examples:
|
36
34
|
|
37
35
|
# handling nil (None serves as NullObject)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
36
|
+
Maybe(nil).a.b.c == Nothing
|
37
|
+
|
38
|
+
# Nothing
|
39
|
+
Maybe(nil)._ == Nothing
|
40
|
+
"#{Maybe(nil)}" == "Nothing"
|
41
|
+
Maybe(nil)._("unknown") == "unknown"
|
42
|
+
Maybe(nil).empty? == true
|
43
|
+
Maybe(nil).truly? == false
|
44
|
+
|
45
|
+
# Just stays Just, unless you unbox it
|
46
|
+
Maybe('FOO').downcase == Just('foo')
|
47
|
+
Maybe('FOO').downcase.fetch == "foo" # unboxing the value
|
48
|
+
Maybe('FOO').downcase._ == "foo"
|
49
|
+
Maybe('foo').empty? == false # always non-empty
|
50
|
+
Maybe('foo').truly? == true # depends on the boxed value
|
51
|
+
Maybe(false).empty? == false
|
52
|
+
Maybe(false).truly? == false
|
54
53
|
|
55
54
|
Map, select:
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
Maybe(123).map { |value| User.find(value) } == Just(someUser) # if user found
|
57
|
+
Maybe(0).map { |value| User.find(value) } == Nothing # if user not found
|
58
|
+
Maybe([1,2]).map { |value| value.to_s } == Just(["1", "2"]) # for all Enumerables
|
60
59
|
|
61
|
-
|
62
|
-
|
60
|
+
Maybe('foo').select { |value| value.start_with?('f') } == Just('foo')
|
61
|
+
Maybe('bar').select { |value| value.start_with?('f') } == Nothing
|
63
62
|
|
64
63
|
Treat it like an array:
|
65
64
|
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
Maybe(123).to_a == [123]
|
66
|
+
Maybe([123, 456]).to_a == [123, 456]
|
67
|
+
Maybe(nil).to_a == []
|
69
68
|
|
70
69
|
Falsey values (kind-of) examples:
|
71
70
|
|
72
|
-
user =
|
71
|
+
user = Maybe(User.find(123))
|
73
72
|
user.name._
|
74
73
|
|
75
|
-
user.fetch('You are not logged in') { |user| "You are logged in as #{user.name}" }.should == 'You are logged in as foo'
|
76
|
-
|
77
|
-
if user != nil
|
78
|
-
"You are logged in as foo"
|
79
|
-
else
|
80
|
-
"You are not logged in"
|
81
|
-
|
82
74
|
user.subscribed? # always true
|
83
75
|
user.subscribed?.truly? # true if subscribed is true
|
84
76
|
user.subscribed?.fetch(false) # same as above
|
@@ -105,7 +97,7 @@ Slug example
|
|
105
97
|
|
106
98
|
# do it with a default
|
107
99
|
def slug(title)
|
108
|
-
|
100
|
+
Maybe(title).strip.downcase.tr_s('^[a-z0-9]', '-')._('unknown-title')
|
109
101
|
end
|
110
102
|
|
111
103
|
### Either
|
@@ -244,6 +236,11 @@ Or install it yourself as:
|
|
244
236
|
|
245
237
|
$ gem install monadic
|
246
238
|
|
239
|
+
## Compatibility
|
240
|
+
Monadic is tested under ruby MRI 1.9.2, 1.9.3, jruby 1.9 mode, rbx 1.9 mode.
|
241
|
+
|
242
|
+
See the build status [](http://travis-ci.org/pzol/monadic)
|
243
|
+
|
247
244
|
## Contributing
|
248
245
|
|
249
246
|
1. Fork it
|
data/Rakefile
CHANGED
@@ -1,2 +1,11 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
2
|
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc "Run all specs"
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
7
|
+
t.pattern = FileList['spec/**/*_spec.rb']
|
8
|
+
t.rspec_opts = %w[--format doc --color]
|
9
|
+
end
|
10
|
+
|
11
|
+
task :default => :spec
|
data/lib/monadic/either.rb
CHANGED
@@ -1,93 +1,108 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module Monadic
|
2
|
+
# @abstract Chains function calls and stops executing if one of them fails.
|
3
|
+
class Either < Monad
|
4
|
+
def self.chain(initial=nil, &block)
|
5
|
+
Either::Chain.new(&block).call(initial)
|
6
|
+
end
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
def self.unit(value)
|
9
|
+
return Failure.new(value) if value.nil? || (value.respond_to?(:empty?) && value.empty?) || !value
|
10
|
+
return Success.new(value)
|
11
|
+
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
13
|
+
# Initialize is private, because it always would return an instance of Either, but Success or Failure
|
14
|
+
# are required (Either is abstract).
|
15
|
+
def initialize(value)
|
16
|
+
raise NoMethodError, "private method `new' called for #{self.class.name}, use `unit' instead"
|
17
|
+
end
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
return @value
|
18
|
-
end
|
19
|
-
alias :_ :fetch
|
20
|
-
|
21
|
-
def bind(proc=nil, &block)
|
22
|
-
return self if failure?
|
23
|
-
|
24
|
-
begin
|
25
|
-
result = if proc && proc.arity == 0
|
26
|
-
then proc.call
|
27
|
-
else (proc || block).call(@value)
|
28
|
-
end
|
29
|
-
result ||= Failure(nil)
|
30
|
-
result = Either(result) unless result.is_a? Either
|
31
|
-
result
|
32
|
-
rescue Exception => ex
|
33
|
-
Failure(ex)
|
19
|
+
def success?
|
20
|
+
is_a? Success
|
34
21
|
end
|
35
|
-
end
|
36
|
-
alias :>= :bind
|
37
|
-
alias :+ :bind
|
38
22
|
|
39
|
-
|
40
|
-
|
41
|
-
|
23
|
+
def failure?
|
24
|
+
is_a? Failure
|
25
|
+
end
|
42
26
|
|
43
|
-
|
44
|
-
|
45
|
-
|
27
|
+
def fetch(default=@value)
|
28
|
+
return default if failure?
|
29
|
+
return @value
|
30
|
+
end
|
31
|
+
alias :_ :fetch
|
32
|
+
|
33
|
+
def bind(proc=nil, &block)
|
34
|
+
return self if failure?
|
35
|
+
|
36
|
+
begin
|
37
|
+
result = if proc && proc.arity == 0
|
38
|
+
then proc.call
|
39
|
+
else (proc || block).call(@value)
|
40
|
+
end
|
41
|
+
result ||= Failure(nil)
|
42
|
+
result = Either(result) unless result.is_a? Either
|
43
|
+
result
|
44
|
+
rescue Exception => ex
|
45
|
+
Failure(ex)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
alias :>= :bind
|
49
|
+
alias :+ :bind
|
46
50
|
end
|
47
51
|
|
48
|
-
|
52
|
+
class Either::Chain
|
53
|
+
def initialize(&block)
|
54
|
+
@chain = []
|
55
|
+
instance_eval(&block)
|
56
|
+
end
|
49
57
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
58
|
+
def call(initial)
|
59
|
+
@chain.inject(Success(initial)) do |result, current|
|
60
|
+
result.bind(current)
|
61
|
+
end
|
62
|
+
end
|
55
63
|
|
56
|
-
|
57
|
-
|
58
|
-
result.bind(current)
|
64
|
+
def bind(proc=nil, &block)
|
65
|
+
@chain << (proc || block)
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
|
-
|
63
|
-
|
69
|
+
# @private instance and class methods for Success and Failure
|
70
|
+
module SuccessFailure
|
71
|
+
module ClassMethods
|
72
|
+
def unit(value)
|
73
|
+
new(value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
module InstanceMethods
|
77
|
+
def initialize(value)
|
78
|
+
@value = join(value)
|
79
|
+
end
|
80
|
+
end
|
64
81
|
end
|
65
82
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
include Either
|
70
|
-
def initialize(value)
|
71
|
-
@value = value
|
83
|
+
class Success < Either
|
84
|
+
extend SuccessFailure::ClassMethods
|
85
|
+
include SuccessFailure::InstanceMethods
|
72
86
|
end
|
73
|
-
end
|
74
87
|
|
75
|
-
class Failure
|
76
|
-
|
77
|
-
|
78
|
-
@value = value
|
88
|
+
class Failure < Either
|
89
|
+
extend SuccessFailure::ClassMethods
|
90
|
+
include SuccessFailure::InstanceMethods
|
79
91
|
end
|
80
|
-
end
|
81
92
|
|
82
|
-
def
|
83
|
-
|
84
|
-
end
|
93
|
+
def Failure(value)
|
94
|
+
Failure.new(value)
|
95
|
+
end
|
85
96
|
|
86
|
-
|
87
|
-
|
88
|
-
|
97
|
+
# Factory method
|
98
|
+
# @return [Success]
|
99
|
+
def Success(value)
|
100
|
+
Success.new(value)
|
101
|
+
end
|
89
102
|
|
90
|
-
|
91
|
-
return Failure
|
92
|
-
|
103
|
+
# Magic factory
|
104
|
+
# @return [Success, Failure] depending whether +value+ is falsey
|
105
|
+
def Either(value)
|
106
|
+
Either.unit(value)
|
107
|
+
end
|
93
108
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Monadic
|
2
|
+
# @api private helps treating `Maybe` like Either in Scala
|
3
|
+
module ScalaStuff
|
4
|
+
def map(proc = nil, &block)
|
5
|
+
return Maybe(@value.map(&block)) if @value.is_a?(Enumerable)
|
6
|
+
return Maybe((proc || block).call(@value))
|
7
|
+
end
|
8
|
+
|
9
|
+
def select(proc = nil, &block)
|
10
|
+
return Maybe(@value.select(&block)) if @value.is_a?(Enumerable)
|
11
|
+
return Nothing unless (proc || block).call(@value)
|
12
|
+
return self
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Maybe < Monad
|
17
|
+
include ScalaStuff
|
18
|
+
|
19
|
+
def self.unit(value)
|
20
|
+
return Nothing if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
21
|
+
return Just.new(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Initialize is private, because it always would return an instance of Maybe, but Just or Nothing
|
25
|
+
# are required (Maybe is abstract).
|
26
|
+
def initialize(*args)
|
27
|
+
raise NoMethodError, "private method `new' called for #{self.class.name}, use `unit' instead"
|
28
|
+
end
|
29
|
+
|
30
|
+
def empty?
|
31
|
+
@value.respond_to?(:empty?) && @value.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_ary
|
35
|
+
return [@value].flatten if @value.respond_to? :flatten
|
36
|
+
return [@value]
|
37
|
+
end
|
38
|
+
alias :to_a :to_ary
|
39
|
+
|
40
|
+
def truly?
|
41
|
+
@value == true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Just < Maybe
|
46
|
+
def initialize(value)
|
47
|
+
@value = join(value)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch(default=nil)
|
51
|
+
@value
|
52
|
+
end
|
53
|
+
alias :_ :fetch
|
54
|
+
|
55
|
+
def method_missing(m, *args)
|
56
|
+
Maybe(@value.__send__(m, *args))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Nothing < Maybe
|
61
|
+
class << self
|
62
|
+
def fetch(default=nil)
|
63
|
+
return self if default.nil?
|
64
|
+
return default
|
65
|
+
end
|
66
|
+
alias :_ :fetch
|
67
|
+
|
68
|
+
def method_missing(m, *args)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_ary
|
73
|
+
[]
|
74
|
+
end
|
75
|
+
alias :to_a :to_ary
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
'Nothing'
|
79
|
+
end
|
80
|
+
|
81
|
+
def truly?
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def Maybe(value)
|
88
|
+
Maybe.unit(value)
|
89
|
+
end
|
90
|
+
alias :Just :Maybe
|
91
|
+
alias :Nothing :Maybe
|
92
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Monadic
|
2
|
+
class Monad
|
3
|
+
def self.unit(value)
|
4
|
+
new(value)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(value)
|
8
|
+
@value = join(value)
|
9
|
+
end
|
10
|
+
|
11
|
+
def bind(proc=nil, &block)
|
12
|
+
(proc || block).call(@value)
|
13
|
+
end
|
14
|
+
|
15
|
+
# If the passed value is monad already, get the value to avoid nesting
|
16
|
+
# M[M[A]] is equivalent to M[A]
|
17
|
+
def join(value)
|
18
|
+
if value.is_a? self.class then value.fetch
|
19
|
+
else value end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Unwraps the the Monad
|
23
|
+
def fetch
|
24
|
+
@value
|
25
|
+
end
|
26
|
+
alias :_ :fetch
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
pretty_class_name = self.class.name.split('::')[-1]
|
30
|
+
"#{pretty_class_name}(#{@value.nil? ? 'nil' : @value.to_s})"
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
return false unless other.is_a? self.class
|
35
|
+
@value == other.instance_variable_get(:@value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/monadic/validation.rb
CHANGED
@@ -1,24 +1,28 @@
|
|
1
|
-
|
2
|
-
Validation
|
3
|
-
|
4
|
-
|
5
|
-
# Conducts several function calls which do checks, of which each must return Success or Failure
|
6
|
-
# and returns a list of all failures.
|
7
|
-
# Validation is not a monad, but an deemed an applicative functor
|
8
|
-
class Validation
|
9
|
-
def initialize
|
10
|
-
@result = Success([])
|
1
|
+
module Monadic
|
2
|
+
# Wraps the construction of the Validation class
|
3
|
+
def Validation(&block)
|
4
|
+
Validation.new.call(&block)
|
11
5
|
end
|
12
6
|
|
13
|
-
|
14
|
-
|
15
|
-
|
7
|
+
# Conducts several function calls which do checks, of which each must return Success or Failure
|
8
|
+
# and returns a list of all failures.
|
9
|
+
# Validation is not a monad, but an deemed an applicative functor
|
10
|
+
class Validation
|
11
|
+
def initialize
|
12
|
+
@result = Success([])
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(&block)
|
16
|
+
instance_eval(&block)
|
17
|
+
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
#
|
20
|
+
def check(proc=nil, &block)
|
21
|
+
result = (proc || block).call
|
22
|
+
raise NotEitherError, "Expected #{result.inspect} to be an Either" unless result.is_a? Either
|
23
|
+
@result = Failure(@result.fetch << result.fetch) if result.failure?
|
21
24
|
|
22
|
-
|
25
|
+
@result
|
26
|
+
end
|
23
27
|
end
|
24
28
|
end
|
data/lib/monadic/version.rb
CHANGED
data/lib/monadic.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'monadic/version'
|
2
2
|
|
3
3
|
require 'monadic/errors'
|
4
|
-
require 'monadic/
|
4
|
+
require 'monadic/monad'
|
5
|
+
require 'monadic/maybe'
|
5
6
|
require 'monadic/either'
|
6
7
|
require 'monadic/validation'
|
7
8
|
|
@@ -9,5 +10,3 @@ module Monadic
|
|
9
10
|
end
|
10
11
|
|
11
12
|
include Monadic
|
12
|
-
None = Monadic::None
|
13
|
-
Some = Monadic::Some
|
data/monadic.gemspec
CHANGED
@@ -4,7 +4,7 @@ require File.expand_path('../lib/monadic/version', __FILE__)
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
5
|
gem.authors = ["Piotr Zolnierek"]
|
6
6
|
gem.email = ["pz@anixe.pl"]
|
7
|
-
gem.description = %q{brings some functional goodness to ruby}
|
7
|
+
gem.description = %q{brings some functional goodness to ruby by giving you some monads}
|
8
8
|
gem.summary = %q{see README}
|
9
9
|
gem.homepage = "http://github.com/pzol/monadic"
|
10
10
|
|
@@ -21,4 +21,5 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.add_development_dependency 'guard-bundler'
|
22
22
|
gem.add_development_dependency 'growl'
|
23
23
|
gem.add_development_dependency 'activesupport'
|
24
|
+
gem.add_development_dependency 'rake'
|
24
25
|
end
|
data/spec/either_spec.rb
CHANGED
@@ -1,14 +1,34 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe
|
3
|
+
describe Monadic::Either do
|
4
|
+
it 'Either cannot be created using #new, use #unit instead' do
|
5
|
+
expect { Either.new(1) }.to raise_error NoMethodError
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'Success.new and Success.unit and Success() return the same' do
|
9
|
+
Success(1).should == Success.unit(1)
|
10
|
+
Success.new(1).should == Success.unit(1)
|
11
|
+
|
12
|
+
Success(nil).should == Success.unit(nil)
|
13
|
+
Success.new(nil).should == Success.unit(nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'Failure.new and Failure.unit and Failure() return the same' do
|
17
|
+
Failure(1).should == Failure.unit(1)
|
18
|
+
Failure.new(1).should == Failure.unit(1)
|
19
|
+
|
20
|
+
Failure(nil).should == Failure.unit(nil)
|
21
|
+
Failure.new(nil).should == Failure.unit(nil)
|
22
|
+
end
|
23
|
+
|
4
24
|
it 'Success and Failure should be kind of Either' do
|
5
|
-
Success.
|
6
|
-
Failure.
|
25
|
+
Success.unit(0).should be_kind_of(Either)
|
26
|
+
Failure.unit(0).should be_kind_of(Either)
|
7
27
|
end
|
8
28
|
|
9
29
|
it '#to_s works' do
|
10
|
-
Success.
|
11
|
-
Failure.
|
30
|
+
Success.unit("it worked!").to_s.should == "Success(it worked!)"
|
31
|
+
Failure.unit(nil).to_s.should == "Failure(nil)"
|
12
32
|
end
|
13
33
|
|
14
34
|
it 'allows to verify equality' do
|
@@ -190,7 +210,7 @@ describe 'Either' do
|
|
190
210
|
bind {|path| load_file(path) }.
|
191
211
|
bind {|content| process_content(content) }
|
192
212
|
either.should be_a_failure
|
193
|
-
|
213
|
+
either.fetch.should be_a KeyError
|
194
214
|
end
|
195
215
|
|
196
216
|
it 'instance variables' do
|
data/spec/jruby_fixes.rb
ADDED
data/spec/maybe_spec.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Monadic::Maybe do
|
4
|
+
it_behaves_like 'a Monad' do
|
5
|
+
let(:monad) { Maybe }
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'Maybe cannot be created using #new, use #unit instead' do
|
9
|
+
expect { Maybe.new(1) }.to raise_error NoMethodError
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'nil as value always returns Nothing()' do
|
13
|
+
Maybe(nil).a.b.c.should == Nothing
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'on non-existant methods, returns Nothing' do
|
17
|
+
Maybe({}).a.b.c.should == Nothing
|
18
|
+
end
|
19
|
+
|
20
|
+
describe Monadic::Nothing do
|
21
|
+
it_behaves_like 'a Monad' do
|
22
|
+
let(:monad) { Nothing }
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'Nothing stays Nothing' do
|
26
|
+
Maybe(nil).fetch.should == Nothing
|
27
|
+
Maybe(nil)._.should == Nothing
|
28
|
+
Maybe(nil).empty?.should be_true
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'Nothing#to_s is "Nothing"' do
|
32
|
+
option = Maybe(nil)
|
33
|
+
"#{option}".should == "Nothing"
|
34
|
+
Nothing.to_s.should == "Nothing"
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'Nothing is always empty' do
|
38
|
+
Nothing.empty?.should be_true
|
39
|
+
Maybe(nil).empty?.should be_true
|
40
|
+
end
|
41
|
+
|
42
|
+
it '[] as value always returns Nothing()' do
|
43
|
+
Maybe([]).a.should == Nothing
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'is always empty and false' do
|
47
|
+
Nothing.empty?.should be_true
|
48
|
+
Nothing.truly?.should be_false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe Monadic::Just do
|
53
|
+
it_behaves_like 'a Monad' do
|
54
|
+
let(:monad) { Just }
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'Just stays Just' do
|
58
|
+
Maybe('foo').should be_kind_of(Just)
|
59
|
+
Maybe('foo').empty?.should be_false
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'Just#to_s is "Just(value)"' do
|
63
|
+
Just.unit(123).to_s.should == "Just(123)"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'calling methods on Maybe always returns an Maybe with the transformed value' do
|
68
|
+
Maybe('FOO').downcase.should == Just('foo')
|
69
|
+
end
|
70
|
+
|
71
|
+
it '#fetch returns the value of an option' do
|
72
|
+
Maybe('foo').fetch.should == 'foo'
|
73
|
+
Maybe('foo')._.should == 'foo'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'returns the value of an option with a default, in case value is Nothing' do
|
77
|
+
Maybe(nil).fetch('bar').should == 'bar'
|
78
|
+
Maybe(nil)._('bar').should == 'bar'
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns the value and not the default if it is Just' do
|
82
|
+
Maybe('FOO').downcase.fetch('bar').should == 'foo'
|
83
|
+
Maybe('FOO').downcase._('bar').should == 'foo'
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'is never falsey' do
|
87
|
+
Maybe('foo').should_not be_false
|
88
|
+
Maybe(nil).should_not be_false
|
89
|
+
Maybe(false).truly?.should be_false
|
90
|
+
Maybe(true).truly?.should be_true
|
91
|
+
Maybe(nil).truly?.should be_false
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'handles (kind-of) falsey values' do
|
95
|
+
FalseyUser = Struct.new(:name, :subscribed)
|
96
|
+
user = Maybe(FalseyUser.new(name = 'foo', subscribed = true))
|
97
|
+
user.subscribed.fetch(false).should be_true
|
98
|
+
user.subscribed.truly?.should be_true
|
99
|
+
|
100
|
+
user = Maybe(nil)
|
101
|
+
user.subscribed.fetch(false).should be_false
|
102
|
+
user.subscribed.truly?.should be_false
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'allows to use map' do
|
106
|
+
Maybe(nil).map { |e| Hash.new(:key => e) }.should == Nothing
|
107
|
+
Maybe('foo').map { |e| Hash.new(:key => e) }.should == Just(Hash.new(:key => 'foo'))
|
108
|
+
Maybe([1,2]).map { |e| e.to_s }.should == Just(["1", "2"])
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'allows to use select' do
|
112
|
+
Maybe('foo').select { |e| e.start_with?('f') }.should == Just('foo')
|
113
|
+
Maybe('bar').select { |e| e.start_with?('f') }.should == Nothing
|
114
|
+
Maybe(nil).select { |e| e.never_called }.should == Nothing
|
115
|
+
Maybe([1, 2]).select { |e| e == 1 }.should == Just([1])
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'acts as an array' do
|
119
|
+
Maybe('foo').to_a.should == ['foo']
|
120
|
+
Maybe(['foo', 'bar']).to_a.should == ['foo', 'bar']
|
121
|
+
Maybe(nil).to_a.should == []
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'diving into hashes' do
|
125
|
+
Maybe({})['a']['b']['c'].should == Nothing
|
126
|
+
Maybe({a: 1})[:a]._.should == 1
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should support Rumonades example' do
|
130
|
+
require 'active_support/time'
|
131
|
+
def format_date_in_march(time_or_date_or_nil)
|
132
|
+
Maybe(time_or_date_or_nil). # wraps possibly-nil value in an Maybe monad (Some or Nothing)
|
133
|
+
map(&:to_date). # transforms a contained Time value into a Date value
|
134
|
+
select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes Nothing)
|
135
|
+
map(&:to_s). # transforms a contained Date value into a String value
|
136
|
+
map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
|
137
|
+
fetch("not in march!") # returns the contained value, or the alternative if Nothing
|
138
|
+
end
|
139
|
+
|
140
|
+
format_date_in_march(nil).should == "not in march!"
|
141
|
+
format_date_in_march(Time.parse('2009-01-01 01:02')).should == "not in march!"
|
142
|
+
format_date_in_march(Time.parse('2011-03-21 12:34')).should == "20110321"
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_examples 'a Monad' do
|
4
|
+
describe 'axioms' do
|
5
|
+
it '1st monadic law: left-identity' do
|
6
|
+
f = ->(value) { monad.unit(value + 1) }
|
7
|
+
monad::unit(1).bind do |value|
|
8
|
+
f.(value)
|
9
|
+
end.should == f.(1)
|
10
|
+
end
|
11
|
+
|
12
|
+
it '2nd monadic law: right-identy - unit and bind do not change the value' do
|
13
|
+
monad.unit(1).bind do |value|
|
14
|
+
monad.unit(value)
|
15
|
+
end.should == monad.unit(1)
|
16
|
+
end
|
17
|
+
|
18
|
+
it '3rd monadic law: associativity' do
|
19
|
+
f = ->(value) { monad.unit(value + 1) }
|
20
|
+
g = ->(value) { monad.unit(value + 100) }
|
21
|
+
|
22
|
+
id1 = monad.unit(1).bind do |a|
|
23
|
+
f.(a)
|
24
|
+
end.bind do |b|
|
25
|
+
g.(b)
|
26
|
+
end
|
27
|
+
|
28
|
+
id2 = monad.unit(1).bind do |a|
|
29
|
+
f.(a).bind do |b|
|
30
|
+
g.(b)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
id1.should == id2
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/monad_spec.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Monadic::Monad do
|
4
|
+
it_behaves_like 'a Monad' do
|
5
|
+
let(:monad) { Monad }
|
6
|
+
end
|
7
|
+
|
8
|
+
it '#to_s shows the monad name and its value' do
|
9
|
+
Monad.unit(1).to_s.should == 'Monad(1)'
|
10
|
+
Monad.unit(nil).to_s.should == 'Monad(nil)'
|
11
|
+
end
|
12
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/validation_spec.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: monadic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05-
|
12
|
+
date: 2012-05-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -107,7 +107,23 @@ dependencies:
|
|
107
107
|
- - ! '>='
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '0'
|
110
|
-
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rake
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: brings some functional goodness to ruby by giving you some monads
|
111
127
|
email:
|
112
128
|
- pz@anixe.pl
|
113
129
|
executables: []
|
@@ -116,6 +132,7 @@ extra_rdoc_files: []
|
|
116
132
|
files:
|
117
133
|
- .gitignore
|
118
134
|
- .rspec
|
135
|
+
- .travis.yml
|
119
136
|
- CHANGELOG.md
|
120
137
|
- Gemfile
|
121
138
|
- Guardfile
|
@@ -125,12 +142,16 @@ files:
|
|
125
142
|
- lib/monadic.rb
|
126
143
|
- lib/monadic/either.rb
|
127
144
|
- lib/monadic/errors.rb
|
128
|
-
- lib/monadic/
|
145
|
+
- lib/monadic/maybe.rb
|
146
|
+
- lib/monadic/monad.rb
|
129
147
|
- lib/monadic/validation.rb
|
130
148
|
- lib/monadic/version.rb
|
131
149
|
- monadic.gemspec
|
132
150
|
- spec/either_spec.rb
|
133
|
-
- spec/
|
151
|
+
- spec/jruby_fixes.rb
|
152
|
+
- spec/maybe_spec.rb
|
153
|
+
- spec/monad_axioms.rb
|
154
|
+
- spec/monad_spec.rb
|
134
155
|
- spec/spec_helper.rb
|
135
156
|
- spec/validation_spec.rb
|
136
157
|
homepage: http://github.com/pzol/monadic
|
@@ -159,6 +180,10 @@ specification_version: 3
|
|
159
180
|
summary: see README
|
160
181
|
test_files:
|
161
182
|
- spec/either_spec.rb
|
162
|
-
- spec/
|
183
|
+
- spec/jruby_fixes.rb
|
184
|
+
- spec/maybe_spec.rb
|
185
|
+
- spec/monad_axioms.rb
|
186
|
+
- spec/monad_spec.rb
|
163
187
|
- spec/spec_helper.rb
|
164
188
|
- spec/validation_spec.rb
|
189
|
+
has_rdoc:
|
data/lib/monadic/option.rb
DELETED
@@ -1,101 +0,0 @@
|
|
1
|
-
require 'singleton'
|
2
|
-
# Represents optional values. Instances of Option are either an instance of Some or the object None.
|
3
|
-
#
|
4
|
-
|
5
|
-
# Helper function which returns Some or None respectively, depending on their value
|
6
|
-
# I find this moar simplistic in ruby than the traditional #bind and #unit
|
7
|
-
def Option(value)
|
8
|
-
return Monadic::None if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
9
|
-
return Monadic::Some.new(value)
|
10
|
-
end
|
11
|
-
alias :Some :Option
|
12
|
-
alias :Maybe :Option
|
13
|
-
|
14
|
-
|
15
|
-
# Represents the Option if there is some value available
|
16
|
-
module Monadic
|
17
|
-
class Some
|
18
|
-
def initialize(value)
|
19
|
-
@value = value
|
20
|
-
end
|
21
|
-
|
22
|
-
def to_ary
|
23
|
-
return [@value].flatten if @value.respond_to? :flatten
|
24
|
-
return [@value]
|
25
|
-
end
|
26
|
-
alias :to_a :to_ary
|
27
|
-
|
28
|
-
def empty?
|
29
|
-
false
|
30
|
-
end
|
31
|
-
|
32
|
-
def truly?
|
33
|
-
@value == true
|
34
|
-
end
|
35
|
-
|
36
|
-
def fetch(default=None, &block)
|
37
|
-
return block.call(@value) if block_given?
|
38
|
-
return @value
|
39
|
-
end
|
40
|
-
alias :or :fetch
|
41
|
-
alias :_ :fetch
|
42
|
-
|
43
|
-
def map(func = nil, &block)
|
44
|
-
return Option(@value.map(&block)) if @value.is_a?(Enumerable)
|
45
|
-
return Option((func || block).call(@value))
|
46
|
-
end
|
47
|
-
|
48
|
-
def method_missing(m, *args)
|
49
|
-
Option(@value.__send__(m, *args))
|
50
|
-
end
|
51
|
-
|
52
|
-
def select(func = nil, &block)
|
53
|
-
return Option(@value.select(&block)) if @value.is_a?(Enumerable)
|
54
|
-
return None unless (func || block).call(@value)
|
55
|
-
return self
|
56
|
-
end
|
57
|
-
|
58
|
-
def to_s
|
59
|
-
"Some(#{@value.to_s})"
|
60
|
-
end
|
61
|
-
|
62
|
-
def ==(other)
|
63
|
-
return false unless other.is_a? Some
|
64
|
-
@value == other.instance_variable_get(:@value)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# Represents the Option if there is no value available
|
69
|
-
class None
|
70
|
-
class << self
|
71
|
-
def to_ary
|
72
|
-
[]
|
73
|
-
end
|
74
|
-
alias :to_a :to_ary
|
75
|
-
|
76
|
-
def empty?
|
77
|
-
true
|
78
|
-
end
|
79
|
-
|
80
|
-
def fetch(default=nil)
|
81
|
-
raise NoValueError if default.nil?
|
82
|
-
default
|
83
|
-
end
|
84
|
-
alias :or :fetch
|
85
|
-
alias :_ :fetch
|
86
|
-
|
87
|
-
def method_missing(m, *args)
|
88
|
-
self
|
89
|
-
end
|
90
|
-
|
91
|
-
def to_s
|
92
|
-
'None'
|
93
|
-
end
|
94
|
-
|
95
|
-
def truly?
|
96
|
-
false
|
97
|
-
end
|
98
|
-
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
data/spec/option_spec.rb
DELETED
@@ -1,129 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe 'Option' do
|
4
|
-
it 'nil as value always returns None()' do
|
5
|
-
Option(nil).a.b.c.should == None
|
6
|
-
end
|
7
|
-
|
8
|
-
it 'None stays None' do
|
9
|
-
expect { Option(nil)._ }.to raise_error Monadic::NoValueError
|
10
|
-
Option(nil).empty?.should be_true
|
11
|
-
end
|
12
|
-
|
13
|
-
it 'Some stays Some' do
|
14
|
-
Option('foo').should be_kind_of(Some)
|
15
|
-
Option('foo').empty?.should be_false
|
16
|
-
end
|
17
|
-
|
18
|
-
it 'Some#to_s is "Some(value)"' do
|
19
|
-
Some(123).to_s.should == "Some(123)"
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'None#to_s is "None"' do
|
23
|
-
option = Option(nil)
|
24
|
-
"#{option}".should == "None"
|
25
|
-
end
|
26
|
-
|
27
|
-
it 'None is always empty' do
|
28
|
-
None.empty?.should be_true
|
29
|
-
Maybe(nil).empty?.should be_true
|
30
|
-
end
|
31
|
-
|
32
|
-
it '[] as value always returns None()' do
|
33
|
-
Option([]).a.should == None
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'calling methods on Option always returns an Option with the transformed value' do
|
37
|
-
Option('FOO').downcase.should == Some('foo')
|
38
|
-
end
|
39
|
-
|
40
|
-
it '#fetch returns the value of an option' do
|
41
|
-
Option('foo').fetch.should == 'foo'
|
42
|
-
Option('foo')._.should == 'foo'
|
43
|
-
end
|
44
|
-
|
45
|
-
it 'returns the value of an option with a default, in case value is None' do
|
46
|
-
Option(nil).fetch('bar').should == 'bar'
|
47
|
-
Option(nil)._('bar').should == 'bar'
|
48
|
-
end
|
49
|
-
|
50
|
-
it 'returns the value and not the default if it is Some' do
|
51
|
-
Option('FOO').downcase.fetch('bar').should == 'foo'
|
52
|
-
Option('FOO').downcase._('bar').should == 'foo'
|
53
|
-
end
|
54
|
-
|
55
|
-
it 'returns the value applied to a block if it is Some' do
|
56
|
-
Option('foo').fetch('bar') { |val| "You are logged in as #{val}" }.should == 'You are logged in as foo'
|
57
|
-
Option(nil).fetch('You are not logged in') { |val| "You are logged in as #{val}" }.should == 'You are not logged in'
|
58
|
-
end
|
59
|
-
|
60
|
-
it 'is never falsey' do
|
61
|
-
Option('foo').should_not be_false
|
62
|
-
Option(nil).should_not be_false
|
63
|
-
end
|
64
|
-
|
65
|
-
class User
|
66
|
-
attr_reader :name
|
67
|
-
def initialize(name)
|
68
|
-
@name = name
|
69
|
-
end
|
70
|
-
def subscribed?
|
71
|
-
true
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'allows to use a block with fetch and _' do
|
76
|
-
user = Option(User.new('foo'))
|
77
|
-
user.fetch('You are not logged in') { |user| "You are logged in as #{user.name}" }.should == 'You are logged in as foo'
|
78
|
-
end
|
79
|
-
|
80
|
-
it 'handles (kind-of) falsey values' do
|
81
|
-
user = Option(User.new('foo'))
|
82
|
-
user.subscribed?.or(false).should be_true
|
83
|
-
user.subscribed?.truly?.should be_true
|
84
|
-
|
85
|
-
user = Option(nil)
|
86
|
-
user.subscribed?.or(false).should be_false
|
87
|
-
user.subscribed?.truly?.should be_false
|
88
|
-
end
|
89
|
-
|
90
|
-
it 'allows to use map' do
|
91
|
-
Option(nil).map { |e| Hash.new(:key => e) }.should == None
|
92
|
-
Option('foo').map { |e| Hash.new(:key => e) }.should == Some(Hash.new(:key => 'foo'))
|
93
|
-
Option([1,2]).map { |e| e.to_s }.should == Some(["1", "2"])
|
94
|
-
end
|
95
|
-
|
96
|
-
it 'allows to use select' do
|
97
|
-
Option('foo').select { |e| e.start_with?('f') }.should == Some('foo')
|
98
|
-
Option('bar').select { |e| e.start_with?('f') }.should == None
|
99
|
-
Option(nil).select { |e| e.never_called }.should == None
|
100
|
-
Option([1, 2]).select { |e| e == 1 }.should == Some([1])
|
101
|
-
end
|
102
|
-
|
103
|
-
it 'acts as an array' do
|
104
|
-
Option('foo').to_a.should == ['foo']
|
105
|
-
Option(['foo', 'bar']).to_a.should == ['foo', 'bar']
|
106
|
-
Option(nil).to_a.should == []
|
107
|
-
end
|
108
|
-
|
109
|
-
it 'diving into hashes' do
|
110
|
-
Maybe({})['a']['b']['c'].should == None
|
111
|
-
Maybe({a: 1})[:a]._.should == 1
|
112
|
-
end
|
113
|
-
|
114
|
-
it 'should support Rumonades example' do
|
115
|
-
require 'active_support/time'
|
116
|
-
def format_date_in_march(time_or_date_or_nil)
|
117
|
-
Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
|
118
|
-
map(&:to_date). # transforms a contained Time value into a Date value
|
119
|
-
select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
|
120
|
-
map(&:to_s). # transforms a contained Date value into a String value
|
121
|
-
map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
|
122
|
-
fetch("not in march!") # returns the contained value, or the alternative if None
|
123
|
-
end
|
124
|
-
|
125
|
-
format_date_in_march(nil).should == "not in march!"
|
126
|
-
format_date_in_march(Time.parse('2009-01-01 01:02')).should == "not in march!"
|
127
|
-
format_date_in_march(Time.parse('2011-03-21 12:34')).should == "20110321"
|
128
|
-
end
|
129
|
-
end
|