monadic 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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, [![Build Status](https://secure.travis-ci.org/pzol/monadic.png?branch=master)](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 [![Build Status](https://secure.travis-ci.org/pzol/monadic.png?branch=master)](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
|