oscillo 0.0.1
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +133 -0
- data/Rakefile +14 -0
- data/lib/oscillo/bool_signal.rb +76 -0
- data/lib/oscillo/combine.rb +44 -0
- data/lib/oscillo/enumerable.rb +183 -0
- data/lib/oscillo/num_signal.rb +96 -0
- data/lib/oscillo/signal.rb +187 -0
- data/lib/oscillo/version.rb +3 -0
- data/lib/oscillo.rb +8 -0
- data/oscillo.gemspec +20 -0
- data/test/test_bool_signal.rb +44 -0
- data/test/test_combine.rb +47 -0
- data/test/test_enumerable.rb +137 -0
- data/test/test_num_signal.rb +96 -0
- data/test/test_signal.rb +145 -0
- metadata +91 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2012 Evan Tatarka
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Oscillo
|
|
2
|
+
|
|
3
|
+
Based off of functional reactive programming, oscillo gives you a signal that
|
|
4
|
+
represents a value changing over time. You can manipulate signals and combine
|
|
5
|
+
them together in various ways.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
gem 'oscillo'
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install oscillo
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Creating and using signals
|
|
24
|
+
|
|
25
|
+
Creating a signal is as simple as
|
|
26
|
+
|
|
27
|
+
s = Oscillo::Signal.new
|
|
28
|
+
|
|
29
|
+
You modify the value of the signal with `s << :value` and you get the current
|
|
30
|
+
value of the signal with `s.value`, or `s.val` if you want to save a couple of
|
|
31
|
+
letters.
|
|
32
|
+
|
|
33
|
+
If you want to give a signal a starting value, you just pass it to the
|
|
34
|
+
constructor.
|
|
35
|
+
|
|
36
|
+
s = Oscillo::Signal.new(0)
|
|
37
|
+
|
|
38
|
+
### Following other signals
|
|
39
|
+
|
|
40
|
+
If you pass another signal as an argument to `new`, the signal's value will
|
|
41
|
+
follow the other one.
|
|
42
|
+
|
|
43
|
+
a = Oscillo::Signal.new
|
|
44
|
+
b = Oscillo::Signal.new(a)
|
|
45
|
+
|
|
46
|
+
a << :value
|
|
47
|
+
b.val #=> :value
|
|
48
|
+
|
|
49
|
+
You can pass a block to `new` to modify how the new signal follows the old.
|
|
50
|
+
|
|
51
|
+
a = Oscillo::Signal.new(0)
|
|
52
|
+
b = Oscillo::Signal.new(a) { |v| v * 2 }
|
|
53
|
+
|
|
54
|
+
a << 3
|
|
55
|
+
b.val #=> 6
|
|
56
|
+
|
|
57
|
+
You can also follow multiple signals at once. The new signal will change if any
|
|
58
|
+
of the signal that you follow changes.
|
|
59
|
+
|
|
60
|
+
a = Oscillo::Signal.new(0)
|
|
61
|
+
b = Oscillo::Signal.new(0)
|
|
62
|
+
c = Oscillo::Signal.new(a, b) { |v1, v2| v1 + v2 }
|
|
63
|
+
|
|
64
|
+
a << 2
|
|
65
|
+
c.val #=> 2
|
|
66
|
+
b << 3
|
|
67
|
+
c.val #=> 5
|
|
68
|
+
|
|
69
|
+
### Useful methods in the block
|
|
70
|
+
|
|
71
|
+
The last argument given to the block passed to new is the signal itself. This is
|
|
72
|
+
so you utilize other methods to query the signal and modify it's changed value.
|
|
73
|
+
|
|
74
|
+
{Oscillo::Signal#abort} aborts the new value change, keeping the old one.
|
|
75
|
+
|
|
76
|
+
a = Oscillo::Signal.new
|
|
77
|
+
b = Oscillo::Signal.new(a) { |v, s| s.abort if v == :bad; v }
|
|
78
|
+
|
|
79
|
+
a << :good
|
|
80
|
+
b.val #=> :good
|
|
81
|
+
a << :bad
|
|
82
|
+
b.val #=> :bad
|
|
83
|
+
|
|
84
|
+
{Oscillo::Signal#source} gives the original signal that caused the cascade of
|
|
85
|
+
changes. This is useful if you are following multiple signals and want to know
|
|
86
|
+
which one actually changed.
|
|
87
|
+
|
|
88
|
+
a = Oscillo::Signal.new
|
|
89
|
+
b = Oscillo::Signal.new
|
|
90
|
+
c = Oscillo::Signal.new(a, b) do |v1, v2, s|
|
|
91
|
+
"The last change was to #{s.source.val}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
a << 1
|
|
95
|
+
c.val #=> "The last change was to 1"
|
|
96
|
+
b << 2
|
|
97
|
+
c.val #=> "The last change was to 2"
|
|
98
|
+
|
|
99
|
+
### Combining signals
|
|
100
|
+
|
|
101
|
+
You can combine signals together in different ways. For example,
|
|
102
|
+
{Oscillo::Combine.either} updates the new signal to the value of whichever was
|
|
103
|
+
the last signal to change.
|
|
104
|
+
|
|
105
|
+
a = Oscillo::Signal.new
|
|
106
|
+
b = Oscillo::Signal.new
|
|
107
|
+
c = Oscillo::Combine.either(a, b)
|
|
108
|
+
a << "a changed"
|
|
109
|
+
c.val #=> "a changed"
|
|
110
|
+
b << "b changed"
|
|
111
|
+
c.val #=> "b changed"
|
|
112
|
+
|
|
113
|
+
See {Oscillo::Combine} for all combination methods.
|
|
114
|
+
|
|
115
|
+
### Enumerable
|
|
116
|
+
|
|
117
|
+
A signal can thought of a sequence of values over time. Therefore, {Oscillo::Signal}
|
|
118
|
+
implements a large number of Enumerable's methods. For example,
|
|
119
|
+
|
|
120
|
+
a = Oscillo::Signal.new(0)
|
|
121
|
+
b = a.map { |v| v ** 2 }
|
|
122
|
+
a << 3
|
|
123
|
+
b.val #=> 9
|
|
124
|
+
|
|
125
|
+
See {Oscillo::Enumerable} for all the methods implemented.
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
1. Fork it
|
|
130
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
131
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
132
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
133
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Oscillo
|
|
2
|
+
# A signal that operates on boolean values.
|
|
3
|
+
class BoolSignal < Signal
|
|
4
|
+
# (see Signal#initialize)
|
|
5
|
+
# Defaults to false instead of nil.
|
|
6
|
+
def initialize(*, &block)
|
|
7
|
+
@value = false; super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Negates the signal. i.e if the original signal was true, the new signal is
|
|
11
|
+
# false and vice versa.
|
|
12
|
+
#
|
|
13
|
+
# @return [Signal]
|
|
14
|
+
def not
|
|
15
|
+
self.class.new(self) { |v| !v }
|
|
16
|
+
end
|
|
17
|
+
alias_method '!', :not
|
|
18
|
+
|
|
19
|
+
# The new signal is true if all of the given signals are true, otherwise it
|
|
20
|
+
# is false.
|
|
21
|
+
#
|
|
22
|
+
# @param signals [[Signal]]
|
|
23
|
+
# @return [Signal]
|
|
24
|
+
def and(*signals)
|
|
25
|
+
self.class.new(self, *signals) { |*vs, s| vs.all? }
|
|
26
|
+
end
|
|
27
|
+
alias_method :&, :and
|
|
28
|
+
|
|
29
|
+
# The new signal is true if all of the given signals are false, otherwise it
|
|
30
|
+
# is true.
|
|
31
|
+
#
|
|
32
|
+
# @param signals [[Signal]]
|
|
33
|
+
# @return [Signal]
|
|
34
|
+
def nand(*signals)
|
|
35
|
+
self.and(*signals).not
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The new signal is true if any of the given signals are true, otherwise it
|
|
39
|
+
# is false.
|
|
40
|
+
#
|
|
41
|
+
# @param signals [[Signal]]
|
|
42
|
+
# @return [Signal]
|
|
43
|
+
def or(*signals)
|
|
44
|
+
self.class.new(self, *signals) { |*vs, s| vs.any? }
|
|
45
|
+
end
|
|
46
|
+
alias_method :|, :or
|
|
47
|
+
|
|
48
|
+
# The new signal is true if any of the given signals are false, otherwise it
|
|
49
|
+
# is false.
|
|
50
|
+
#
|
|
51
|
+
# @param signals [[Signal]]
|
|
52
|
+
# @return [Signal]
|
|
53
|
+
def nor(*signals)
|
|
54
|
+
self.or(*signals).not
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The new signal is true if only one of the given signals are true,
|
|
58
|
+
# otherwise it is false.
|
|
59
|
+
#
|
|
60
|
+
# @param signal [Signal]
|
|
61
|
+
# @return [Signal]
|
|
62
|
+
def xor(signal)
|
|
63
|
+
self.class.new(self, signal) { |a, b| a ^ b }
|
|
64
|
+
end
|
|
65
|
+
alias_method :^, :xor
|
|
66
|
+
|
|
67
|
+
# The new signal is true if only one of the given signals are false,
|
|
68
|
+
# otherwise it is false.
|
|
69
|
+
#
|
|
70
|
+
# @param signal [Signal]
|
|
71
|
+
# @return [Signal]
|
|
72
|
+
def xnor(signal)
|
|
73
|
+
self.xor(signal).not
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Oscillo
|
|
2
|
+
# Constructs new signals by combining multiple signals together in different
|
|
3
|
+
# ways.
|
|
4
|
+
module Combine
|
|
5
|
+
# When any of the signals change, update the new signal with the value of
|
|
6
|
+
# the last changed signal.
|
|
7
|
+
#
|
|
8
|
+
# @param signals [[Signal]] the signals to update on
|
|
9
|
+
# @return [Signal]
|
|
10
|
+
def self.either(*signals)
|
|
11
|
+
Signal.new(*signals) do |*vs, s|
|
|
12
|
+
s.source.val
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Updates the signal when any of the given signals change and the block
|
|
17
|
+
# evaluates to true. The value is the value of the last changed signal.
|
|
18
|
+
# @see when_not
|
|
19
|
+
#
|
|
20
|
+
# @param signals [[Signal]] the signals to update on
|
|
21
|
+
# @yield [value] the new value of the last changed signal
|
|
22
|
+
# @return [Signal]
|
|
23
|
+
def self.when(*signals, &block)
|
|
24
|
+
Signal.new(*signals) do |*vs, s|
|
|
25
|
+
s.abort unless block.(s.source.val)
|
|
26
|
+
s.source.val
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Updates the signal when any of the given signals change and the block
|
|
31
|
+
# evaluates to false. The value is the value of the last changed signal.
|
|
32
|
+
# @see when
|
|
33
|
+
#
|
|
34
|
+
# @param signals [[Signal]] the signals to update on
|
|
35
|
+
# @yield [value] the new value of the last changed signal
|
|
36
|
+
# @return [Signal]
|
|
37
|
+
def self.when_not(*signals, &block)
|
|
38
|
+
Signal.new(*signals) do |*vs, s|
|
|
39
|
+
s.abort if block.(s.source.val)
|
|
40
|
+
s.source.val
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
module Oscillo
|
|
2
|
+
# Adds enumerable-like methods to Signal.
|
|
3
|
+
module Enumerable
|
|
4
|
+
NO_ARG = Object.new
|
|
5
|
+
private_constant :NO_ARG
|
|
6
|
+
|
|
7
|
+
# Returns a signal that is the result of running the values of the original
|
|
8
|
+
# signal through the block.
|
|
9
|
+
#
|
|
10
|
+
# @yield [*signals] gives the new values of the signals to the block
|
|
11
|
+
# @return [Signal]
|
|
12
|
+
def collect(&block)
|
|
13
|
+
self.class.new(self) { |*v, s| block.(*v) }
|
|
14
|
+
end
|
|
15
|
+
alias_method :map, :collect
|
|
16
|
+
|
|
17
|
+
# Returns a signal that changes only when the original signal changes and
|
|
18
|
+
# the block is not false.
|
|
19
|
+
# @see #reject
|
|
20
|
+
#
|
|
21
|
+
# @yield [value] gives the new value of the signal to the block
|
|
22
|
+
# @return [Signal]
|
|
23
|
+
def find_all(&block)
|
|
24
|
+
self.class.new(self) do |v, s|
|
|
25
|
+
s.abort unless block.(v); v
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
alias_method :select, :find_all
|
|
29
|
+
|
|
30
|
+
# Returns a signal that changes only when the original signal changes and
|
|
31
|
+
# the block is false.
|
|
32
|
+
# @see #find_all
|
|
33
|
+
#
|
|
34
|
+
# @yield [value] give the new value of the signal to the block
|
|
35
|
+
# @return [Signal]
|
|
36
|
+
def reject(&block)
|
|
37
|
+
self.class.new(self) do |v, s|
|
|
38
|
+
s.abort if block.(v); v
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Passes each value of the signal to the given block. The value of the
|
|
43
|
+
# resulting signal is true if the block never returns false or nil. If the
|
|
44
|
+
# block is not given, uses an implicit block of {|value| value} (that is
|
|
45
|
+
# {#all?} will return true only if none of the signal values were false or
|
|
46
|
+
# nil.)
|
|
47
|
+
# @note Only takes in to consideration values from the time the signal was
|
|
48
|
+
# created.
|
|
49
|
+
# @see #any?
|
|
50
|
+
#
|
|
51
|
+
# @yield [new_value] give the new value of the signal to the block
|
|
52
|
+
# @return [Signal<Boolean>]
|
|
53
|
+
def all?(&block)
|
|
54
|
+
self.class.new(true, self) do |v, s|
|
|
55
|
+
v = block.(v) if block_given?
|
|
56
|
+
s.val & v
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Passes each value of the signal to the given block. The value of the
|
|
61
|
+
# resulting signal is true if the block ever returns a value other than
|
|
62
|
+
# false or nil. If the block is not given, uses an implicit block of
|
|
63
|
+
# {|value| value} (that is {#any?} will return true if at least one of the
|
|
64
|
+
# signal values were not false or nil.)
|
|
65
|
+
# @note Only takes in to consideration values from the time the signal was
|
|
66
|
+
# created.
|
|
67
|
+
# @see #all?
|
|
68
|
+
#
|
|
69
|
+
# @yield [new_value] give the new value of the signal to the block
|
|
70
|
+
# @return [Signal<Boolean>]
|
|
71
|
+
def any?(&block)
|
|
72
|
+
self.class.new(false, self) do |v, s|
|
|
73
|
+
s.val | block.(v)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Take the value from this signal and merges it with the values of the given
|
|
78
|
+
# signals to form an array.
|
|
79
|
+
#
|
|
80
|
+
# @param signals [[Signal]] the other signals to merge in the array
|
|
81
|
+
# @return [Signal]
|
|
82
|
+
def zip(*signals)
|
|
83
|
+
self.class.new(self, *signals) {|*v, s| v }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Equivalent to {Signal#on_change}
|
|
87
|
+
#
|
|
88
|
+
# @yield (see Signal#on_change)
|
|
89
|
+
# @return [Signal]
|
|
90
|
+
def each(&block)
|
|
91
|
+
self.on_change(&block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the number values that the signal has had. If an argument is
|
|
95
|
+
# given, counts the number of values in the signal, for which equals to
|
|
96
|
+
# item. If a block is given, counts the number of values yielding a true
|
|
97
|
+
# value.
|
|
98
|
+
# @note Only takes in to consideration values from the time the signal was
|
|
99
|
+
# created.
|
|
100
|
+
#
|
|
101
|
+
# @overload count
|
|
102
|
+
# @return number of values that the signal had
|
|
103
|
+
# @overload count(obj)
|
|
104
|
+
# @param obj values to count
|
|
105
|
+
# @return number of values equal to obj that the signal had
|
|
106
|
+
# @overload count
|
|
107
|
+
# @yield [value] gives the new value to the block
|
|
108
|
+
# @return number of values for which the block evaluates to true
|
|
109
|
+
def count(obj=NO_ARG, &block)
|
|
110
|
+
return count_with_arg(obj) unless obj.equal?(NO_ARG)
|
|
111
|
+
return count_with_block(&block) if block_given?
|
|
112
|
+
|
|
113
|
+
self.class.new(0, self) do |v, s|
|
|
114
|
+
s.val + 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Combines all values of the signal by applying a binary operation,
|
|
119
|
+
# specified by a block or a symbol that names a method or operator. If you
|
|
120
|
+
# specify a block, then for each value in this signal the block is passed an
|
|
121
|
+
# accumulator value (memo) the new value. If you specify a symbol instead,
|
|
122
|
+
# then each new value will be passed to the named method of memo. In either
|
|
123
|
+
# case, the result becomes the new value for memo.
|
|
124
|
+
#
|
|
125
|
+
# @overload inject
|
|
126
|
+
# @yield [memo, value] gives the memo and the new value of the signal to the
|
|
127
|
+
# block
|
|
128
|
+
# @overload inject(symbol)
|
|
129
|
+
# @param symbol that represents method to call on memo
|
|
130
|
+
# @overload inject(initial)
|
|
131
|
+
# @param initial the start value of the memo
|
|
132
|
+
# @yield [memo, value] gives the memo and the new value of the signal to the
|
|
133
|
+
# block
|
|
134
|
+
# @overload inject(initial, symbol)
|
|
135
|
+
# @param initial the start value of the memo
|
|
136
|
+
# @param symbol that represents method to call on memo
|
|
137
|
+
# @return [Signal]
|
|
138
|
+
def inject(*args, &block)
|
|
139
|
+
if block_given?
|
|
140
|
+
inject_with_block(*args, &block)
|
|
141
|
+
else
|
|
142
|
+
inject_with_sym(*args)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
alias_method :reduce, :inject
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def count_with_arg(obj)
|
|
150
|
+
self.class.new(0, self) do |v, s|
|
|
151
|
+
obj == v ? s.val + 1 : s.val
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def count_with_block(&block)
|
|
156
|
+
self.class.new(0, self) do |v, s|
|
|
157
|
+
block.(v) ? s.val + 1 : s.val
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def inject_with_sym(initial=NO_ARG, sym)
|
|
162
|
+
first_run = true
|
|
163
|
+
self.class.new(initial, self) do |v, s|
|
|
164
|
+
if first_run and initial.equal?(NO_ARG)
|
|
165
|
+
first_run = false; v
|
|
166
|
+
else
|
|
167
|
+
s.val.send(sym, v)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def inject_with_block(initial=NO_ARG, &block)
|
|
173
|
+
first_run = true
|
|
174
|
+
self.class.new(initial, self) do |v, s|
|
|
175
|
+
if first_run and initial.equal?(NO_ARG)
|
|
176
|
+
first_run = false; v
|
|
177
|
+
else
|
|
178
|
+
block.(s.val, v)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Oscillo
|
|
2
|
+
# A signal that works on numeric values.
|
|
3
|
+
class NumSignal < Signal
|
|
4
|
+
# (see Signal#initialize)
|
|
5
|
+
# Defaults to 0 instead of nil.
|
|
6
|
+
def initialize(*, &block)
|
|
7
|
+
@value = 0; super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# The new signal gives the running total of all values of the original
|
|
11
|
+
# signal.
|
|
12
|
+
#
|
|
13
|
+
# @return [Signal]
|
|
14
|
+
def sum
|
|
15
|
+
self.inject(:+)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The new signal gives the difference between the previous value and the
|
|
19
|
+
# current value of the signal.
|
|
20
|
+
#
|
|
21
|
+
# @return [Signal]
|
|
22
|
+
def delta
|
|
23
|
+
prev = self.val
|
|
24
|
+
self.class.new(self) do |v, s|
|
|
25
|
+
v - prev.tap { prev = v }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The new signal gives the running product of all values of the original
|
|
30
|
+
# signal.
|
|
31
|
+
#
|
|
32
|
+
# @return [Signal]
|
|
33
|
+
def product
|
|
34
|
+
self.inject(:*)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The new signal is the sum of the original signal and the given signal.
|
|
38
|
+
#
|
|
39
|
+
# @param signal [Signal] the signal to sum
|
|
40
|
+
# @return [Signal]
|
|
41
|
+
def plus(signal)
|
|
42
|
+
self.class.new(self, signal) { |a, b| a + b}
|
|
43
|
+
end
|
|
44
|
+
alias_method :+, :plus
|
|
45
|
+
|
|
46
|
+
# The new signal is the difference of the original signal and the given
|
|
47
|
+
# signal.
|
|
48
|
+
#
|
|
49
|
+
# @param signal [Signal] the signal to subtract
|
|
50
|
+
# @return [Signal]
|
|
51
|
+
def minus(signal)
|
|
52
|
+
self.class.new(self, signal) { |a, b| a - b }
|
|
53
|
+
end
|
|
54
|
+
alias_method :-, :minus
|
|
55
|
+
|
|
56
|
+
# The new signal is the product of the original signal and the given
|
|
57
|
+
# signal.
|
|
58
|
+
#
|
|
59
|
+
# @param signal [Signal] the signal to multiply
|
|
60
|
+
# @return [Signal]
|
|
61
|
+
def times(signal)
|
|
62
|
+
self.class.new(self, signal) { |a, b| a * b }
|
|
63
|
+
end
|
|
64
|
+
alias_method :*, :times
|
|
65
|
+
|
|
66
|
+
# The new signal is the division of the original signal and the given
|
|
67
|
+
# signal.
|
|
68
|
+
#
|
|
69
|
+
# @param signal [Signal] the signal to divide
|
|
70
|
+
# @return [Signal]
|
|
71
|
+
def div(signal)
|
|
72
|
+
self.class.new(self, signal) { |a, b| a / b }
|
|
73
|
+
end
|
|
74
|
+
alias_method :/, :div
|
|
75
|
+
|
|
76
|
+
# The new signal is the modulus of the original signal and the given
|
|
77
|
+
# signal.
|
|
78
|
+
#
|
|
79
|
+
# @param signal [Signal] the signal to mod
|
|
80
|
+
# @return [Signal]
|
|
81
|
+
def mod(signal)
|
|
82
|
+
self.class.new(self, signal) { |a, b| a % b }
|
|
83
|
+
end
|
|
84
|
+
alias_method :%, :mod
|
|
85
|
+
|
|
86
|
+
# The new signal is the original signal raised to the power of the given
|
|
87
|
+
# signal.
|
|
88
|
+
#
|
|
89
|
+
# @param signal [Signal] the signal to raise to the power of
|
|
90
|
+
# @return [Signal]
|
|
91
|
+
def pow(signal)
|
|
92
|
+
self.class.new(self, signal) { |a, b| a ** b }
|
|
93
|
+
end
|
|
94
|
+
alias_method :**, :pow
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require 'oscillo/enumerable'
|
|
2
|
+
|
|
3
|
+
module Oscillo
|
|
4
|
+
# A signal that changes over time. This change can either be explicit through
|
|
5
|
+
# the use of {#change} or {#<<}, or it can be implicit when any of the signals
|
|
6
|
+
# that this signal follows changes.
|
|
7
|
+
#
|
|
8
|
+
# @author Evan Tatarka <evan@tatarka.me>
|
|
9
|
+
class Signal
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
# Creates a new Signal with an optional initial value, signals to follow,
|
|
13
|
+
# and block. The block converts the values from any signals that this one
|
|
14
|
+
# follows to the value of this signal. If no block is given, the value will
|
|
15
|
+
# be an array of all the values of the signals that it follows (or if it is
|
|
16
|
+
# just following one signal, the value of that signal).
|
|
17
|
+
#
|
|
18
|
+
# @overload initialize(*signals)
|
|
19
|
+
# Follows some number of other signals, changing when they change.
|
|
20
|
+
# @param signals [Signal] the signals to follow
|
|
21
|
+
# @yield [*signals, self] gives the new values of the signals to the block
|
|
22
|
+
#
|
|
23
|
+
# @overload initialize(start_value, *signals)
|
|
24
|
+
# Starts with an initial value and follows other signals when they change.
|
|
25
|
+
# @param start_value the initial value of this signal
|
|
26
|
+
# @param signals [Signal] the signals to follow
|
|
27
|
+
# @yield [*signals, self] gives the new values of the signals to the block
|
|
28
|
+
def initialize(*values, &block)
|
|
29
|
+
@on_change = []
|
|
30
|
+
@follows = []
|
|
31
|
+
@followed_by = []
|
|
32
|
+
|
|
33
|
+
@block = block || ->(*x, s) { x.size <= 1 ? x.first : x }
|
|
34
|
+
|
|
35
|
+
return if values.empty?
|
|
36
|
+
|
|
37
|
+
# first may be initial value instead of signal
|
|
38
|
+
first, *rest = *values
|
|
39
|
+
if first.is_a?(self.class)
|
|
40
|
+
follow(*values)
|
|
41
|
+
else
|
|
42
|
+
@value = first
|
|
43
|
+
follow(*rest)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Follows additional signals, changing when those signals change. This is
|
|
48
|
+
# necessary to define mutually recursive signals.
|
|
49
|
+
# @note Immediately updates value.
|
|
50
|
+
#
|
|
51
|
+
# @param signals [Signal] the signals to follow
|
|
52
|
+
# @return [self]
|
|
53
|
+
def follow(*signals)
|
|
54
|
+
@follows += signals.each { |signal| signal.followed_by(self) }
|
|
55
|
+
update(self)
|
|
56
|
+
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Stops following the given signals.
|
|
61
|
+
# @note Immediately updates value.
|
|
62
|
+
#
|
|
63
|
+
# @param signals [Signal] the signals to stop following
|
|
64
|
+
# @return [self]
|
|
65
|
+
def dont_follow(*signals)
|
|
66
|
+
@follows -= signals.each { |signal| signal.not_followed_by(self) }
|
|
67
|
+
update(self)
|
|
68
|
+
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Updates the value if this signal to represent the values of the signals
|
|
73
|
+
# that it follows. This is called automatically when dependent signals
|
|
74
|
+
# change so you don't need to call this manually in most cases.
|
|
75
|
+
#
|
|
76
|
+
# @param source [Signal] The signal that initialized the update. If not
|
|
77
|
+
# passed, then this signal will be the source of the update.
|
|
78
|
+
def update(source=nil)
|
|
79
|
+
return if @follows.empty? # nothing to update
|
|
80
|
+
|
|
81
|
+
@source = source
|
|
82
|
+
@aborted = false
|
|
83
|
+
new_value = @block.(*@follows.map(&:value), self)
|
|
84
|
+
self.change(new_value) unless @aborted
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns the current value of the signal
|
|
88
|
+
#
|
|
89
|
+
# @return the current value
|
|
90
|
+
def value
|
|
91
|
+
@value
|
|
92
|
+
end
|
|
93
|
+
alias_method :val, :value
|
|
94
|
+
|
|
95
|
+
# Change the current value of the signal. This will update all signals that
|
|
96
|
+
# follow this on as well as call all {#on_change} callbacks registered.
|
|
97
|
+
#
|
|
98
|
+
# @param new_value the new value of the signal
|
|
99
|
+
# @return [self]
|
|
100
|
+
def change(new_value)
|
|
101
|
+
@value = new_value
|
|
102
|
+
@on_change.each { |c| c.(new_value) }
|
|
103
|
+
|
|
104
|
+
unless self === source
|
|
105
|
+
@followed_by.each { |f| f.update(source || self) }
|
|
106
|
+
end
|
|
107
|
+
@source = nil
|
|
108
|
+
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
alias_method :<<, :change
|
|
112
|
+
|
|
113
|
+
# Register a callback that will be called any time the signal value is
|
|
114
|
+
# changed.
|
|
115
|
+
#
|
|
116
|
+
# @yield [value] gives the new value to the block
|
|
117
|
+
# @return [self]
|
|
118
|
+
def on_change(&block)
|
|
119
|
+
@on_change << block
|
|
120
|
+
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Aborts the change so that the value stays the same, no callbacks
|
|
125
|
+
# registered by {#on_change} are called, and no dependent signals are
|
|
126
|
+
# changed.
|
|
127
|
+
# @note This should only be called in the block passed to {#initialize}.
|
|
128
|
+
def abort
|
|
129
|
+
@aborted = true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The original source of the current run of signal changes. This can be
|
|
133
|
+
# useful to know which signal caused the change if you are following
|
|
134
|
+
# multiple signals.
|
|
135
|
+
# @note This is only defined within the block passed to {#initialize}.
|
|
136
|
+
#
|
|
137
|
+
# @return [Signal] the signal that caused the change
|
|
138
|
+
def source
|
|
139
|
+
@source
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The value of the signal as a string, for convenience.
|
|
143
|
+
#
|
|
144
|
+
# @return [String] the signal value
|
|
145
|
+
def to_s
|
|
146
|
+
value.to_s
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The value of the signal as an integer, for convenience.
|
|
150
|
+
#
|
|
151
|
+
# @return [Fixnum] the signal value
|
|
152
|
+
def to_i
|
|
153
|
+
value.to_i
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The new signal ignores change events when the new value is the same as the
|
|
157
|
+
# old value.
|
|
158
|
+
#
|
|
159
|
+
# @return [Signal]
|
|
160
|
+
def drop_repeats
|
|
161
|
+
self.class.new(self) do |v, s|
|
|
162
|
+
s.abort if s.val == v; v
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Updates the new signal to the value of this signal any time the value of
|
|
167
|
+
# sample changes.
|
|
168
|
+
#
|
|
169
|
+
# @param sample [Signal] the signal to update on
|
|
170
|
+
# @return [Signal]
|
|
171
|
+
def sample_on(sample)
|
|
172
|
+
Signal.new(self.val, sample) do |v, s|
|
|
173
|
+
self.val
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
protected
|
|
178
|
+
|
|
179
|
+
def followed_by(follower)
|
|
180
|
+
@followed_by << follower
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def not_followed_by(follower)
|
|
184
|
+
@followed_by.delete(follower)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
data/lib/oscillo.rb
ADDED
data/oscillo.gemspec
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/oscillo/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["Evan Tatarka"]
|
|
6
|
+
gem.email = ["evan@tatarka.me"]
|
|
7
|
+
gem.description = %q{Modify and respond to signals, inspired by functional reactive programming}
|
|
8
|
+
gem.summary = %q{Modify and respond to signals, inspired by functional reactive programming}
|
|
9
|
+
gem.homepage = "http://www.github.com/evant/oscillo"
|
|
10
|
+
|
|
11
|
+
gem.files = `git ls-files`.split($\)
|
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
14
|
+
gem.name = "oscillo"
|
|
15
|
+
gem.require_paths = ["lib"]
|
|
16
|
+
gem.version = Oscillo::VERSION
|
|
17
|
+
|
|
18
|
+
gem.add_development_dependency 'yard', '~> 0.8.3'
|
|
19
|
+
gem.add_development_dependency 'redcarpet', '~> 2.2.2'
|
|
20
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'oscillo'
|
|
3
|
+
|
|
4
|
+
class TestBoolSignal < Test::Unit::TestCase
|
|
5
|
+
include Oscillo
|
|
6
|
+
|
|
7
|
+
def self.test_bool(method, expected)
|
|
8
|
+
a = BoolSignal.new
|
|
9
|
+
b = BoolSignal.new
|
|
10
|
+
c = a.send(method, b)
|
|
11
|
+
|
|
12
|
+
define_method("test_#{method}") do
|
|
13
|
+
i = 0
|
|
14
|
+
[true, false].each do |a_val|
|
|
15
|
+
[true, false].each do |b_val|
|
|
16
|
+
a << a_val; b << b_val
|
|
17
|
+
assert_equal expected[i], c.val
|
|
18
|
+
i += 1
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_default_value
|
|
25
|
+
assert_equal false, BoolSignal.new.val
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_not
|
|
29
|
+
a = BoolSignal.new
|
|
30
|
+
b = a.not
|
|
31
|
+
|
|
32
|
+
a << true
|
|
33
|
+
assert_equal false, b.val
|
|
34
|
+
a << false
|
|
35
|
+
assert_equal true, b.val
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test_bool :and, [true, false, false, false]
|
|
39
|
+
test_bool :nand, [false, true, true, true ]
|
|
40
|
+
test_bool :or, [true, true, true, false]
|
|
41
|
+
test_bool :nor, [false, false, false, true ]
|
|
42
|
+
test_bool :xor, [false, true, true, false]
|
|
43
|
+
test_bool :xnor, [true, false, false, true ]
|
|
44
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'oscillo'
|
|
3
|
+
|
|
4
|
+
class TestCombine < Test::Unit::TestCase
|
|
5
|
+
include Oscillo
|
|
6
|
+
|
|
7
|
+
def test_either
|
|
8
|
+
a = Signal.new
|
|
9
|
+
b = Signal.new
|
|
10
|
+
c = Combine::either(a, b)
|
|
11
|
+
|
|
12
|
+
a << :a
|
|
13
|
+
assert_equal :a, c.val
|
|
14
|
+
b << :b
|
|
15
|
+
assert_equal :b, c.val
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_when
|
|
19
|
+
a = Signal.new
|
|
20
|
+
b = Signal.new
|
|
21
|
+
c = Combine::when(a, b) { |x| x != :bad }
|
|
22
|
+
|
|
23
|
+
a << :a
|
|
24
|
+
assert_equal :a, c.val
|
|
25
|
+
a << :bad
|
|
26
|
+
assert_equal :a, c.val
|
|
27
|
+
b << :b
|
|
28
|
+
assert_equal :b, c.val
|
|
29
|
+
b << :bad
|
|
30
|
+
assert_equal :b, c.val
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_when_not
|
|
34
|
+
a = Signal.new
|
|
35
|
+
b = Signal.new
|
|
36
|
+
c = Combine::when_not(a, b) { |x| x == :bad }
|
|
37
|
+
|
|
38
|
+
a << :a
|
|
39
|
+
assert_equal :a, c.val
|
|
40
|
+
a << :bad
|
|
41
|
+
assert_equal :a, c.val
|
|
42
|
+
b << :b
|
|
43
|
+
assert_equal :b, c.val
|
|
44
|
+
b << :bad
|
|
45
|
+
assert_equal :b, c.val
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'oscillo'
|
|
3
|
+
|
|
4
|
+
class TestEnumerable < Test::Unit::TestCase
|
|
5
|
+
include Oscillo
|
|
6
|
+
|
|
7
|
+
def test_collect
|
|
8
|
+
a = Signal.new(0)
|
|
9
|
+
b = a.map { |x| x * 2 }
|
|
10
|
+
|
|
11
|
+
a << 2
|
|
12
|
+
assert_equal 2 * 2, b.val
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_find_all
|
|
16
|
+
a = Signal.new
|
|
17
|
+
b = a.find_all { |x| x == :good }
|
|
18
|
+
|
|
19
|
+
a << :good
|
|
20
|
+
assert_equal :good, b.val
|
|
21
|
+
a << :bad
|
|
22
|
+
assert_equal :good, b.val
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_select
|
|
26
|
+
test_find_all
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_reject
|
|
30
|
+
a = Signal.new
|
|
31
|
+
b = a.reject { |x| x == :bad }
|
|
32
|
+
|
|
33
|
+
a << :good
|
|
34
|
+
assert_equal :good, b.val
|
|
35
|
+
b << :bad
|
|
36
|
+
assert_equal :bad, b.val
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_all?
|
|
40
|
+
a = Signal.new(:good)
|
|
41
|
+
b = a.all? { |x| x == :good }
|
|
42
|
+
|
|
43
|
+
assert b.val
|
|
44
|
+
a << :bad
|
|
45
|
+
assert !b.val
|
|
46
|
+
a << :good
|
|
47
|
+
assert !b.val
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_any?
|
|
51
|
+
a = Signal.new
|
|
52
|
+
b = a.any? { |x| x == :good }
|
|
53
|
+
|
|
54
|
+
a << :bad
|
|
55
|
+
assert !b.val
|
|
56
|
+
a << :good
|
|
57
|
+
assert b.val
|
|
58
|
+
a << :bad
|
|
59
|
+
assert b.val
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_zip
|
|
63
|
+
a = Signal.new
|
|
64
|
+
b = Signal.new
|
|
65
|
+
c = a.zip(b)
|
|
66
|
+
|
|
67
|
+
a << :a
|
|
68
|
+
b << :b
|
|
69
|
+
assert_equal [:a, :b], c.val
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_inject_with_block
|
|
73
|
+
a = Signal.new("a")
|
|
74
|
+
b = a.inject { |str, value| str + value }
|
|
75
|
+
|
|
76
|
+
assert_equal "a", b.val
|
|
77
|
+
a << "b"
|
|
78
|
+
assert_equal "ab", b.val
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_inject_with_symbol
|
|
82
|
+
a = Signal.new(0)
|
|
83
|
+
b = a.inject(:+)
|
|
84
|
+
|
|
85
|
+
a << 1
|
|
86
|
+
assert_equal 1, b.val
|
|
87
|
+
a << 2
|
|
88
|
+
assert_equal 3, b.val
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_inject_with_initial_and_block
|
|
92
|
+
a = Signal.new(:a)
|
|
93
|
+
b = a.inject([]) { |array, value| array << value }
|
|
94
|
+
|
|
95
|
+
assert_equal [:a], b.val
|
|
96
|
+
a << :b
|
|
97
|
+
assert_equal [:a, :b], b.val
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_inject_with_initial_and_symbol
|
|
101
|
+
a = Signal.new(1)
|
|
102
|
+
b = a.inject(0, :+)
|
|
103
|
+
|
|
104
|
+
assert_equal 1, b.val
|
|
105
|
+
a << 2
|
|
106
|
+
assert_equal 3, b.val
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_count
|
|
110
|
+
a = Signal.new(:a)
|
|
111
|
+
b = a.count
|
|
112
|
+
|
|
113
|
+
assert_equal 1, b.val
|
|
114
|
+
a << :b
|
|
115
|
+
assert_equal 2, b.val
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_count_with_arg
|
|
119
|
+
a = Signal.new
|
|
120
|
+
b = a.count(:good)
|
|
121
|
+
|
|
122
|
+
a << :good
|
|
123
|
+
assert_equal 1, b.val
|
|
124
|
+
a << :bad
|
|
125
|
+
assert_equal 1, b.val
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_count_with_block
|
|
129
|
+
a = Signal.new
|
|
130
|
+
b = a.count { |x| x == :good }
|
|
131
|
+
|
|
132
|
+
a << :good
|
|
133
|
+
assert_equal 1, b.val
|
|
134
|
+
a << :bad
|
|
135
|
+
assert_equal 1, b.val
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'oscillo'
|
|
3
|
+
|
|
4
|
+
class TestNumSignal < Test::Unit::TestCase
|
|
5
|
+
include Oscillo
|
|
6
|
+
|
|
7
|
+
def test_default_value
|
|
8
|
+
assert_equal 0, NumSignal.new.val
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_sum
|
|
12
|
+
a = NumSignal.new(0)
|
|
13
|
+
b = a.sum
|
|
14
|
+
|
|
15
|
+
a << 1
|
|
16
|
+
assert_equal 1, b.val
|
|
17
|
+
a << 2
|
|
18
|
+
assert_equal 3, b.val
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_delta
|
|
22
|
+
a = NumSignal.new(0)
|
|
23
|
+
b = a.delta
|
|
24
|
+
|
|
25
|
+
a << 5
|
|
26
|
+
assert_equal 5, b.val
|
|
27
|
+
a << 2
|
|
28
|
+
assert_equal(-3, b.val)
|
|
29
|
+
a << 4
|
|
30
|
+
assert_equal 2, b.val
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_product
|
|
34
|
+
a = NumSignal.new(1)
|
|
35
|
+
b = a.product
|
|
36
|
+
|
|
37
|
+
a << 2
|
|
38
|
+
assert_equal 2, b.val
|
|
39
|
+
a << 3
|
|
40
|
+
assert_equal 6, b.val
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_plus
|
|
44
|
+
a = NumSignal.new
|
|
45
|
+
b = NumSignal.new
|
|
46
|
+
c = a + b
|
|
47
|
+
|
|
48
|
+
a << 1; b << 2
|
|
49
|
+
assert_equal 3, c.val
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_minus
|
|
53
|
+
a = NumSignal.new
|
|
54
|
+
b = NumSignal.new
|
|
55
|
+
c = a - b
|
|
56
|
+
|
|
57
|
+
a << 5; b << 2
|
|
58
|
+
assert_equal 3, c.val
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_mult
|
|
62
|
+
a = NumSignal.new
|
|
63
|
+
b = NumSignal.new
|
|
64
|
+
c = a * b
|
|
65
|
+
|
|
66
|
+
a << 2; b << 3
|
|
67
|
+
assert_equal 6, c.val
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_div
|
|
71
|
+
a = NumSignal.new
|
|
72
|
+
b = NumSignal.new(1) # Prevent division by 0
|
|
73
|
+
c = a / b
|
|
74
|
+
|
|
75
|
+
a << 6; b << 3
|
|
76
|
+
assert_equal 2, c.val
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_mod
|
|
80
|
+
a = NumSignal.new
|
|
81
|
+
b = NumSignal.new(1) # Prevent division by 0
|
|
82
|
+
c = a % b
|
|
83
|
+
|
|
84
|
+
a << 12; b << 5
|
|
85
|
+
assert_equal 2, c.val
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_pow
|
|
89
|
+
a = NumSignal.new
|
|
90
|
+
b = NumSignal.new
|
|
91
|
+
c = a ** b
|
|
92
|
+
|
|
93
|
+
a << 2; b << 3
|
|
94
|
+
assert_equal 8, c.val
|
|
95
|
+
end
|
|
96
|
+
end
|
data/test/test_signal.rb
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'oscillo'
|
|
3
|
+
|
|
4
|
+
class TestSignal < Test::Unit::TestCase
|
|
5
|
+
include Oscillo
|
|
6
|
+
|
|
7
|
+
def test_initial_value
|
|
8
|
+
assert_equal :test, Signal.new(:test).val
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_change_value
|
|
12
|
+
a = Signal.new
|
|
13
|
+
a << :test
|
|
14
|
+
|
|
15
|
+
assert_equal :test, a.val
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_follows
|
|
19
|
+
a = Signal.new
|
|
20
|
+
b = Signal.new(a)
|
|
21
|
+
|
|
22
|
+
a << :test
|
|
23
|
+
assert_equal :test, b.val
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_follows_initial_value
|
|
27
|
+
a = Signal.new(:a)
|
|
28
|
+
b = Signal.new(a)
|
|
29
|
+
|
|
30
|
+
assert_equal :a, b.val
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_follows_with_block
|
|
34
|
+
a = Signal.new(0)
|
|
35
|
+
b = Signal.new(a) { |x| x * 2 }
|
|
36
|
+
|
|
37
|
+
a << 2
|
|
38
|
+
assert_equal 2 * 2, b.val
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_follows_initial_value_with_block
|
|
42
|
+
a = Signal.new(2)
|
|
43
|
+
b = Signal.new(a) { |x| x * 2 }
|
|
44
|
+
|
|
45
|
+
assert_equal 2 * 2, b.val
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_follows_multiple
|
|
49
|
+
a = Signal.new
|
|
50
|
+
b = Signal.new
|
|
51
|
+
c = Signal.new(a, b)
|
|
52
|
+
|
|
53
|
+
a << :test1
|
|
54
|
+
b << :test2
|
|
55
|
+
assert_equal [:test1, :test2], c.val
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_follows_multiple_with_block
|
|
59
|
+
a = Signal.new(0)
|
|
60
|
+
b = Signal.new(0)
|
|
61
|
+
c = Signal.new(a, b) { |x, y| x + y }
|
|
62
|
+
|
|
63
|
+
a << 2
|
|
64
|
+
b << 3
|
|
65
|
+
assert_equal 2 + 3, c.val
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_abort_follows
|
|
69
|
+
a = Signal.new
|
|
70
|
+
b = Signal.new(a) { |x| b.abort if x == :bad; x }
|
|
71
|
+
|
|
72
|
+
a << :good
|
|
73
|
+
assert_equal :good, b.val
|
|
74
|
+
a << :bad
|
|
75
|
+
assert_equal :good, b.val
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_chained_follows
|
|
79
|
+
a = Signal.new
|
|
80
|
+
b = Signal.new(a)
|
|
81
|
+
c = Signal.new(b)
|
|
82
|
+
|
|
83
|
+
a << :test
|
|
84
|
+
assert_equal :test, b.val
|
|
85
|
+
assert_equal :test, c.val
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_recursive_follows
|
|
89
|
+
a = Signal.new
|
|
90
|
+
b = Signal.new(a)
|
|
91
|
+
|
|
92
|
+
# should not cause a SystemStackError
|
|
93
|
+
a.follow(b)
|
|
94
|
+
|
|
95
|
+
a << :test_a
|
|
96
|
+
assert_equal :test_a, b.val
|
|
97
|
+
b << :test_b
|
|
98
|
+
assert_equal :test_b, a.val
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_dont_follow
|
|
102
|
+
a = Signal.new
|
|
103
|
+
b = Signal.new(a)
|
|
104
|
+
b.dont_follow(a)
|
|
105
|
+
|
|
106
|
+
a << :test
|
|
107
|
+
assert :test != b.val
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_on_change
|
|
111
|
+
a = Signal.new(:a)
|
|
112
|
+
changes = 0
|
|
113
|
+
a.on_change { changes += 1 }
|
|
114
|
+
|
|
115
|
+
assert_equal 0, changes
|
|
116
|
+
a << :a1
|
|
117
|
+
assert_equal 1, changes
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_sample_on
|
|
121
|
+
a = Signal.new(:start)
|
|
122
|
+
b = Signal.new
|
|
123
|
+
c = a.sample_on(b)
|
|
124
|
+
|
|
125
|
+
a << :new
|
|
126
|
+
assert_equal :start, c.val
|
|
127
|
+
b << :changed
|
|
128
|
+
assert_equal :new, c.val
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_drop_repeats
|
|
132
|
+
a = Signal.new
|
|
133
|
+
b = a.drop_repeats
|
|
134
|
+
|
|
135
|
+
changes = 0
|
|
136
|
+
b.on_change { changes += 1 }
|
|
137
|
+
|
|
138
|
+
a << :a
|
|
139
|
+
assert_equal 1, changes
|
|
140
|
+
a << :a
|
|
141
|
+
assert_equal 1, changes
|
|
142
|
+
a << :b
|
|
143
|
+
assert_equal 2, changes
|
|
144
|
+
end
|
|
145
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: oscillo
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Evan Tatarka
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-11-20 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: yard
|
|
16
|
+
requirement: &13298160 !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ~>
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.8.3
|
|
22
|
+
type: :development
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: *13298160
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: redcarpet
|
|
27
|
+
requirement: &13297620 !ruby/object:Gem::Requirement
|
|
28
|
+
none: false
|
|
29
|
+
requirements:
|
|
30
|
+
- - ~>
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 2.2.2
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: *13297620
|
|
36
|
+
description: Modify and respond to signals, inspired by functional reactive programming
|
|
37
|
+
email:
|
|
38
|
+
- evan@tatarka.me
|
|
39
|
+
executables: []
|
|
40
|
+
extensions: []
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
files:
|
|
43
|
+
- .gitignore
|
|
44
|
+
- Gemfile
|
|
45
|
+
- LICENSE
|
|
46
|
+
- README.md
|
|
47
|
+
- Rakefile
|
|
48
|
+
- lib/oscillo.rb
|
|
49
|
+
- lib/oscillo/bool_signal.rb
|
|
50
|
+
- lib/oscillo/combine.rb
|
|
51
|
+
- lib/oscillo/enumerable.rb
|
|
52
|
+
- lib/oscillo/num_signal.rb
|
|
53
|
+
- lib/oscillo/signal.rb
|
|
54
|
+
- lib/oscillo/version.rb
|
|
55
|
+
- oscillo.gemspec
|
|
56
|
+
- test/test_bool_signal.rb
|
|
57
|
+
- test/test_combine.rb
|
|
58
|
+
- test/test_enumerable.rb
|
|
59
|
+
- test/test_num_signal.rb
|
|
60
|
+
- test/test_signal.rb
|
|
61
|
+
homepage: http://www.github.com/evant/oscillo
|
|
62
|
+
licenses: []
|
|
63
|
+
post_install_message:
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
none: false
|
|
69
|
+
requirements:
|
|
70
|
+
- - ! '>='
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
none: false
|
|
75
|
+
requirements:
|
|
76
|
+
- - ! '>='
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubyforge_project:
|
|
81
|
+
rubygems_version: 1.8.11
|
|
82
|
+
signing_key:
|
|
83
|
+
specification_version: 3
|
|
84
|
+
summary: Modify and respond to signals, inspired by functional reactive programming
|
|
85
|
+
test_files:
|
|
86
|
+
- test/test_bool_signal.rb
|
|
87
|
+
- test/test_combine.rb
|
|
88
|
+
- test/test_enumerable.rb
|
|
89
|
+
- test/test_num_signal.rb
|
|
90
|
+
- test/test_signal.rb
|
|
91
|
+
has_rdoc:
|