Skip to content

Commit d5136bd

Browse files
committed
Added scan operator
1 parent b15fb3f commit d5136bd

File tree

6 files changed

+130
-10
lines changed

6 files changed

+130
-10
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,53 @@ Sx.from(text_edit.text_changed).debounce(0.25).subscribe(func(): print(text_edit
205205
Sx.from(text_edit.text_changed).throttle(0.25).subscribe(func(): print(text_edit.text)) # text will be printed every 0.25 seconds when typing continuously.
206206
```
207207

208+
209+
### Scan operator
210+
Sx allows for scanning and buffering incoming values inside a stateful operator. This operator behavves similar to reduce() in functional programming.
211+
212+
```gdscript
213+
signal numbers(value: int)
214+
215+
Sx.from(numbers).scan(
216+
func(acc: int, value: int):
217+
return acc + value,
218+
0
219+
).subscribe(func(value: int): print(value))
220+
221+
numbers.emit(3)
222+
numbers.emit(2)
223+
numbers.emit(7)
224+
225+
# result:
226+
# 3
227+
# 5
228+
# 12
229+
```
230+
231+
This can also be useful if you want to collect previous emissions:
232+
```gdscript
233+
signal numbers(value: int)
234+
235+
Sx.from(numbers).scan(
236+
func(acc: Array[int], value: int):
237+
acc.append(value)
238+
return acc,
239+
[]
240+
).subscribe(func(value: Array[int]): print(value))
241+
242+
numbers.emit(3)
243+
numbers.emit(2)
244+
numbers.emit(7)
245+
246+
# result:
247+
# [3]
248+
# [3, 2]
249+
# [3, 2, 7]
250+
```
251+
252+
Due to the way the reducing function works, while multiple signal arguments will be passed to the function after the accumulator, this function should return one value.
253+
Subsequent operator after `scan` will receive ONE argument.
254+
208255
### On complete callback
209256
When you're subscribing, you can set an optional callback that will be fired when the signal completes (either naturally, or when signal is disposed).
210257

@@ -455,6 +502,7 @@ for key in dict:
455502
* map
456503
* merge
457504
* merge_from
505+
* scan
458506
* skip
459507
* skip_while
460508
* start_with
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
extends Sx.Operator
2+
3+
4+
var _callable: Callable
5+
var _state: Variant
6+
var _initial_value: Variant
7+
8+
9+
func _init(callable: Callable, initial_value: Variant):
10+
_callable = callable
11+
_initial_value = initial_value
12+
_state = initial_value
13+
14+
15+
func clone() -> Sx.Operator:
16+
return Sx.ScanOperator.new(_callable, _initial_value)
17+
18+
19+
func evaluate(args: Array[Variant]) -> Sx.OperatorResult:
20+
_state = _callable.bindv(args).call(_state)
21+
return Sx.OperatorResult.new(true, [_state])

addons/signal_extensions/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="SignalExtensions"
44
description="Rx-like extensions for Godot's signals."
55
author="Paweł \"TheWalruzz\" Radej"
6-
version="1.7.0"
6+
version="1.8.0"
77
script="plugin.gd"

addons/signal_extensions/signals/sx_signal.gd

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,6 @@ func debounce(debounce_time: float, process_always := true, process_in_physics :
5454
var cloned := clone()
5555
cloned._operators.append(Sx.DebounceOperator.new(debounce_time, process_always, process_in_physics, ignore_timescale))
5656
return cloned
57-
58-
59-
## Throttles the signal by [b]throttle_time[/b].
60-
## This method has arguments consistent with Godot's [method SceneTree.create_timer] method.
61-
## See that method's documentation for more information.
62-
func throttle(throttle_time: float, process_always := true, process_in_physics := false, ignore_timescale := false) -> SxSignal:
63-
var cloned := clone()
64-
cloned._operators.append(Sx.ThrottleOperator.new(throttle_time, process_always, process_in_physics, ignore_timescale))
65-
return cloned
6657

6758

6859
## Delays item emission by [b]duration[/b].
@@ -121,6 +112,14 @@ func merge_from(signals: Array[Signal]) -> SxSignal:
121112
return merge(converted)
122113

123114

115+
## Scans the emitted items and can reduce them to a single value over time.
116+
## Reducing function: func(acc: ACC_TYPE, ...args: Array[Variant]) -> ACC_TYPE
117+
func scan(callable: Callable, initial_value: Variant) -> SxSignal:
118+
var cloned := clone()
119+
cloned._operators.append(Sx.ScanOperator.new(callable, initial_value))
120+
return cloned
121+
122+
124123
## Skips the first [b]item_count[/b] items from the sequence.
125124
func skip(item_count: int) -> SxSignal:
126125
var cloned := clone()
@@ -162,6 +161,15 @@ func take_while(callable: Callable) -> SxSignal:
162161
operator.dispose_callback = cloned._set_dispose
163162
cloned._operators.append(operator)
164163
return cloned
164+
165+
166+
## Throttles the signal by [b]throttle_time[/b].
167+
## This method has arguments consistent with Godot's [method SceneTree.create_timer] method.
168+
## See that method's documentation for more information.
169+
func throttle(throttle_time: float, process_always := true, process_in_physics := false, ignore_timescale := false) -> SxSignal:
170+
var cloned := clone()
171+
cloned._operators.append(Sx.ThrottleOperator.new(throttle_time, process_always, process_in_physics, ignore_timescale))
172+
return cloned
165173

166174

167175
func _clone() -> SxSignal:

addons/signal_extensions/sx_autoload.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const ElementAtOperator := preload("res://addons/signal_extensions/operators/sx_
1111
const FilterOperator := preload("res://addons/signal_extensions/operators/sx_filter_operator.gd")
1212
const FirstOperator := preload("res://addons/signal_extensions/operators/sx_first_operator.gd")
1313
const MapOperator := preload("res://addons/signal_extensions/operators/sx_map_operator.gd")
14+
const ScanOperator := preload("res://addons/signal_extensions/operators/sx_scan_operator.gd")
1415
const SkipOperator := preload("res://addons/signal_extensions/operators/sx_skip_operator.gd")
1516
const SkipWhileOperator := preload("res://addons/signal_extensions/operators/sx_skip_while_operator.gd")
1617
const TakeOperator := preload("res://addons/signal_extensions/operators/sx_take_operator.gd")
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# GdUnit generated TestSuite
2+
class_name SxScanOperatorTest
3+
extends GdUnitTestSuite
4+
@warning_ignore('unused_parameter')
5+
@warning_ignore('return_value_discarded')
6+
7+
# TestSuite generated from
8+
const __source = 'res://addons/signal_extensions/operators/sx_scan_operator.gd'
9+
10+
11+
var operator: Sx.ScanOperator
12+
13+
14+
func test_evaluate_returning_variant() -> void:
15+
operator = Sx.ScanOperator.new(func(acc, value): return acc + value, 0)
16+
var result := operator.evaluate([3])
17+
assert_bool(result.ok).is_true()
18+
assert_array(result.args).has_size(1).contains([3])
19+
result = operator.evaluate([2])
20+
assert_bool(result.ok).is_true()
21+
assert_array(result.args).has_size(1).contains([5])
22+
23+
24+
func test_evaluate_returning_array() -> void:
25+
operator = Sx.ScanOperator.new(func(acc, value):
26+
acc.append(value)
27+
return acc,
28+
[])
29+
var result := operator.evaluate([3])
30+
assert_bool(result.ok).is_true()
31+
assert_array(result.args).has_size(1).contains([[3]])
32+
result = operator.evaluate([2])
33+
assert_bool(result.ok).is_true()
34+
assert_array(result.args).has_size(1).contains([[3, 2]])
35+
36+
37+
func test_cloning() -> void:
38+
operator = Sx.ScanOperator.new(func(acc, value): return acc + value, 0)
39+
var cloned := operator.clone()
40+
assert_object(cloned._callable).is_same(operator._callable)
41+
assert_object(cloned._initial_value).is_same(operator._initial_value)
42+
assert_object(cloned).is_not_same(operator)

0 commit comments

Comments
 (0)