lifo 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +345 -0
- data/lib/lifo/immutable_stack.rb +127 -0
- data/lib/lifo/stack.rb +151 -0
- data/lib/lifo/version.rb +5 -0
- data/lib/lifo.rb +18 -0
- data/sig/lifo/immutable_stack.rbs +31 -0
- data/sig/lifo/stack.rbs +27 -0
- data/sig/lifo/version.rbs +3 -0
- data/sig/lifo.rbs +7 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 47638132a973edf2e698ea0166cf4ab205cf74c6f15c3f754f50011a9925a4d6
|
|
4
|
+
data.tar.gz: 6d98c37b9835607937b7a51c2913ac563d3359ed4c64f0c83876533bc6b87c41
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 14204fbd4a73dc3ba85b80ceef054cf173f4f078d0fd5ddeed376bd662d80f80df03cc7da862e3dd18089bc86cb49364b0c398dc51da283ca12880674cbdff05
|
|
7
|
+
data.tar.gz: f8c968a9049f2e42d37dbef3adc17860660bbfde04f02723eeb9e088e88dc53b11b2ae0e6f020f1bbc7c6835d8657b71c375bfce9031f31722a322e86a3d3cd6
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"lifo" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["TODO: Write your email address"](mailto:"TODO: Write your email address").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leo Arnold
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Lifo
|
|
2
|
+
|
|
3
|
+
**L**ast **I**n, **F**irst **O**ut: implementations of mutable and immutable stacks in pure Ruby.
|
|
4
|
+
|
|
5
|
+
## Table of contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Available stacks](#available-stacks)
|
|
9
|
+
- [Usage](#usage)
|
|
10
|
+
- [Lifo::Stack](#lifostack)
|
|
11
|
+
- [Pushing and popping](#pushing-and-popping)
|
|
12
|
+
- [Empty stacks](#empty-stacks)
|
|
13
|
+
- [Iterating, copying and comparing](#iterating-copying-and-comparing)
|
|
14
|
+
- [Lifo::ImmutableStack](#lifoimmutablestack)
|
|
15
|
+
- [What does "immutable" mean?](#what-does-immutable-mean)
|
|
16
|
+
- [Creating a stack](#creating-a-stack)
|
|
17
|
+
- [Pushing values](#pushing-values)
|
|
18
|
+
- [Looking at the top value](#looking-at-the-top-value)
|
|
19
|
+
- [Removing the top value](#removing-the-top-value)
|
|
20
|
+
- [Empty stacks](#empty-stacks-1)
|
|
21
|
+
- [Keeping old versions ("undo")](#keeping-old-versions-undo)
|
|
22
|
+
- [Iterating and converting](#iterating-and-converting)
|
|
23
|
+
- [Comparing stacks](#comparing-stacks)
|
|
24
|
+
- [A caveat: the values themselves](#a-caveat-the-values-themselves)
|
|
25
|
+
- [Development](#development)
|
|
26
|
+
- [Contributing](#contributing)
|
|
27
|
+
- [License](#license)
|
|
28
|
+
- [Code of Conduct](#code-of-conduct)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bundle add lifo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
gem install lifo
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Available stacks
|
|
45
|
+
|
|
46
|
+
| Class | Description |
|
|
47
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
48
|
+
| [`Lifo::Stack`](#lifostack) | An `Array`-backed stack that is modified in place. `push` and `pop` change the stack you call them on. |
|
|
49
|
+
| [`Lifo::ImmutableStack`](#lifoimmutablestack) | A stack that is never modified. `push` and `pop` return a new stack and leave the original untouched. |
|
|
50
|
+
|
|
51
|
+
Both are `Enumerable`, yield their values from top to bottom, and share the core method names (`push`, `push_all`, `push_all_reverse`, `pop`, `peek`, `size`, `empty?`), but they differ in what those methods do. Only `Lifo::Stack` also offers `clear` and `pop(n)`.
|
|
52
|
+
|
|
53
|
+
| | `Lifo::Stack` | `Lifo::ImmutableStack` |
|
|
54
|
+
|-------------------------------|-------------------------------------|---------------------------------------------|
|
|
55
|
+
| `push(value)` returns | the same stack, now modified | a new stack |
|
|
56
|
+
| `pop` returns | the removed value | a new stack without the top value |
|
|
57
|
+
| `pop` when empty | returns `nil` | raises `Lifo::EmptyStackError` |
|
|
58
|
+
| `peek` when empty | raises `Lifo::EmptyStackError` | raises `Lifo::EmptyStackError` |
|
|
59
|
+
| Creating with values | `Lifo::Stack.new(1, 2)` | `Lifo::ImmutableStack.new.push_all([1, 2])` |
|
|
60
|
+
| Safe to share between threads | no (needs your own locking) | yes (the stack itself, not its values) |
|
|
61
|
+
| Usable as a `Hash` key | only by identity, not by contents | yes, by contents |
|
|
62
|
+
|
|
63
|
+
Because `pop` means something different in each class, do not swap one for the other without checking every call site.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Each stack is documented in its own section below. Load the gem with `require "lifo"` to make all of them available.
|
|
68
|
+
|
|
69
|
+
### Lifo::Stack
|
|
70
|
+
|
|
71
|
+
Use `Lifo::Stack` when you want a plain, cheap stack that you modify in place, like an `Array` used with `push` and `pop`. It is backed by an `Array`, so `push` and `pop` run in (amortized) constant time and nothing is copied.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
require "lifo"
|
|
75
|
+
|
|
76
|
+
stack = Lifo::Stack.new(1, 2) # values are pushed in order, like push_all
|
|
77
|
+
stack << 3
|
|
78
|
+
stack # => #<Lifo::Stack [3, 2, 1] (top first)>
|
|
79
|
+
|
|
80
|
+
stack.peek # => 3
|
|
81
|
+
stack.pop # => 3
|
|
82
|
+
stack.size # => 2
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Pushing and popping
|
|
86
|
+
|
|
87
|
+
`push` (alias `<<`) puts a value on top. `push_all` pushes several values, so the last one ends up on top, while `push_all_reverse` puts the first one on top. All of them modify the stack and return it, so calls can be chained. `push_all_reverse` needs a finite collection, such as an `Array` or a `Range`.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
stack = Lifo::Stack.new
|
|
91
|
+
stack.push(1).push(2) # => #<Lifo::Stack [2, 1] (top first)>
|
|
92
|
+
stack.push_all([3, 4]) # => #<Lifo::Stack [4, 3, 2, 1] (top first)>
|
|
93
|
+
stack.push_all_reverse([5, 6]) # => #<Lifo::Stack [5, 6, 4, 3, 2, 1] (top first)>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`pop` **removes and returns the top value**, just like `Array#pop`. This is the opposite of `Lifo::ImmutableStack#pop`, which returns a new stack. `pop(n)` removes the top `n` values and returns them as an array with the former top value last, like `Array#pop(n)`. That order means `push_all` puts them back:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
stack = Lifo::Stack.new(1, 2, 3)
|
|
100
|
+
|
|
101
|
+
removed = stack.pop(2) # => [2, 3]
|
|
102
|
+
stack # => #<Lifo::Stack [1] (top first)>
|
|
103
|
+
|
|
104
|
+
stack.push_all(removed)
|
|
105
|
+
stack # => #<Lifo::Stack [3, 2, 1] (top first)>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Like `Array#pop(n)`, it returns fewer values if the stack is smaller and never raises for that. Anything other than a non-negative `Integer` raises `ArgumentError`.
|
|
109
|
+
|
|
110
|
+
`clear` removes all values and returns the stack.
|
|
111
|
+
|
|
112
|
+
#### Empty stacks
|
|
113
|
+
|
|
114
|
+
`pop` returns `nil` on an empty stack instead of raising, while `peek` raises `Lifo::EmptyStackError`. Since a stack may also hold `nil` values, use `empty?` to tell an empty stack apart from a popped `nil`:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
stack = Lifo::Stack.new
|
|
118
|
+
|
|
119
|
+
stack.pop # => nil
|
|
120
|
+
stack.peek # raises Lifo::EmptyStackError: cannot peek into an empty stack
|
|
121
|
+
|
|
122
|
+
value = stack.pop unless stack.empty?
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Iterating, copying and comparing
|
|
126
|
+
|
|
127
|
+
`Lifo::Stack` is `Enumerable` and yields its values **from top to bottom**, so `map`, `select`, `include?`, `first`, `to_a` and friends work as expected. Methods like `map` return plain arrays, not stacks.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
stack = Lifo::Stack.new(1, 2, 3)
|
|
131
|
+
|
|
132
|
+
stack.to_a # => [3, 2, 1]
|
|
133
|
+
stack.map { |value| value * 2 } # => [6, 4, 2]
|
|
134
|
+
stack.first # => 3
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `dup` (and `clone`) create an independent copy: pushing to or popping from one does not affect the other. The values themselves are not copied.
|
|
138
|
+
- `==` compares contents and is only true for another `Lifo::Stack` holding equal values in the same order. In particular, a `Lifo::Stack` is never equal to a `Lifo::ImmutableStack`.
|
|
139
|
+
- Since a stack can change, it does not define `hash` or `eql?` and should not be used as a `Hash` key. Use a `Lifo::ImmutableStack` for that.
|
|
140
|
+
- It is not safe to modify from several threads at once without your own locking.
|
|
141
|
+
|
|
142
|
+
### Lifo::ImmutableStack
|
|
143
|
+
|
|
144
|
+
Use `Lifo::ImmutableStack` when you want to pass stacks around freely, keep earlier versions, or share them between threads without worrying about anything changing unexpectedly.
|
|
145
|
+
|
|
146
|
+
#### What does "immutable" mean?
|
|
147
|
+
|
|
148
|
+
With Ruby's built-in `Array`, `push` and `pop` *change* the array you call them on:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
array = [1, 2]
|
|
152
|
+
array.push(3)
|
|
153
|
+
array # => [1, 2, 3] (the original was modified)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
An **immutable** data structure is never modified after it has been created. Instead, every operation leaves the original untouched and returns a **new** value:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
require "lifo"
|
|
160
|
+
|
|
161
|
+
empty = Lifo::ImmutableStack.new
|
|
162
|
+
one = empty.push(1)
|
|
163
|
+
|
|
164
|
+
empty # => #<Lifo::ImmutableStack [] (top first)>
|
|
165
|
+
one # => #<Lifo::ImmutableStack [1] (top first)>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Note that `empty` is still empty after calling `push`. The most common mistake when starting out is to throw away the return value:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
stack = Lifo::ImmutableStack.new
|
|
172
|
+
stack.push(1) # Wrong: the new stack is discarded
|
|
173
|
+
stack.size # => 0
|
|
174
|
+
|
|
175
|
+
stack = stack.push(1) # Right: keep the new stack
|
|
176
|
+
stack.size # => 1
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Why bother? Since nothing can change behind your back, you can hand a stack to any part of your program (or to another thread) without worrying that someone else modifies it. You also get "undo" for free: just keep the old version around. Despite the copying semantics, `push` and `pop` are cheap (constant time), because a new stack shares its contents with the old one instead of duplicating them.
|
|
180
|
+
|
|
181
|
+
#### Creating a stack
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
stack = Lifo::ImmutableStack.new
|
|
185
|
+
|
|
186
|
+
stack.empty? # => true
|
|
187
|
+
stack.size # => 0
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Unlike `Lifo::Stack.new`, the constructor takes no values. To start with some, use `push_all` or `push_all_reverse` (see below).
|
|
191
|
+
|
|
192
|
+
#### Pushing values
|
|
193
|
+
|
|
194
|
+
`push` returns a new stack with the value on top. `<<` is an alias, so calls can be chained:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
stack = Lifo::ImmutableStack.new.push(1).push(2).push(3)
|
|
198
|
+
stack # => #<Lifo::ImmutableStack [3, 2, 1] (top first)>
|
|
199
|
+
|
|
200
|
+
stack = Lifo::ImmutableStack.new << "a" << "b"
|
|
201
|
+
stack # => #<Lifo::ImmutableStack ["b", "a"] (top first)>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
To push many values at once, use `push_all` (the last value ends up on top) or `push_all_reverse` (the first value ends up on top):
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
base = Lifo::ImmutableStack.new.push(0)
|
|
208
|
+
|
|
209
|
+
base.push_all([1, 2, 3]) # => #<Lifo::ImmutableStack [3, 2, 1, 0] (top first)>
|
|
210
|
+
base.push_all_reverse([1, 2, 3]) # => #<Lifo::ImmutableStack [1, 2, 3, 0] (top first)>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`push_all_reverse` needs a finite collection, such as an `Array` or a `Range`.
|
|
214
|
+
|
|
215
|
+
#### Looking at the top value
|
|
216
|
+
|
|
217
|
+
`peek` returns the top value without removing it:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
stack = Lifo::ImmutableStack.new.push(1).push(2)
|
|
221
|
+
|
|
222
|
+
stack.peek # => 2
|
|
223
|
+
stack.size # => 2 (nothing was removed)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Removing the top value
|
|
227
|
+
|
|
228
|
+
`pop` returns a new stack *without* the top value. It does **not** return the removed value, so use `peek` first if you need it:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
stack = Lifo::ImmutableStack.new.push(1).push(2).push(3)
|
|
232
|
+
|
|
233
|
+
top = stack.peek # => 3
|
|
234
|
+
remaining = stack.pop # => #<Lifo::ImmutableStack [2, 1] (top first)>
|
|
235
|
+
|
|
236
|
+
stack # => #<Lifo::ImmutableStack [3, 2, 1] (top first)> (unchanged)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Empty stacks
|
|
240
|
+
|
|
241
|
+
Calling `peek` or `pop` on an empty stack raises `Lifo::EmptyStackError` (a subclass of `Lifo::Error`). Check with `empty?` first if you are not sure:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
stack = Lifo::ImmutableStack.new
|
|
245
|
+
|
|
246
|
+
stack.peek # raises Lifo::EmptyStackError: cannot peek into an empty stack
|
|
247
|
+
stack.pop # raises Lifo::EmptyStackError: cannot pop from an empty stack
|
|
248
|
+
|
|
249
|
+
stack = stack.pop unless stack.empty?
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Keeping old versions ("undo")
|
|
253
|
+
|
|
254
|
+
Because operations never modify a stack, every earlier version stays valid. Branching off from a common starting point is safe:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
history = Lifo::ImmutableStack.new.push("draft 1").push("draft 2")
|
|
258
|
+
|
|
259
|
+
edited = history.push("draft 3")
|
|
260
|
+
reverted = history.pop
|
|
261
|
+
|
|
262
|
+
edited.to_a # => ["draft 3", "draft 2", "draft 1"]
|
|
263
|
+
history.to_a # => ["draft 2", "draft 1"]
|
|
264
|
+
reverted.to_a # => ["draft 1"]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Iterating and converting
|
|
268
|
+
|
|
269
|
+
`ImmutableStack` is `Enumerable` and yields its values **from top to bottom**, so all the usual methods like `map`, `select`, `include?`, `first`, `sum` and `to_a` are available:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
stack = Lifo::ImmutableStack.new.push_all([1, 2, 3])
|
|
273
|
+
|
|
274
|
+
stack.each { |value| puts value } # prints 3, 2, 1
|
|
275
|
+
stack.to_a # => [3, 2, 1]
|
|
276
|
+
stack.map { |value| value * 2 } # => [6, 4, 2]
|
|
277
|
+
stack.include?(2) # => true
|
|
278
|
+
stack.first(2) # => [3, 2]
|
|
279
|
+
stack.sum # => 6
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Methods like `map` return plain arrays, not stacks. To get a stack back, push the result again:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
doubled = Lifo::ImmutableStack.new.push_all_reverse(stack.map { |value| value * 2 })
|
|
286
|
+
doubled # => #<Lifo::ImmutableStack [6, 4, 2] (top first)>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### Comparing stacks
|
|
290
|
+
|
|
291
|
+
Two stacks are equal (`==`, `eql?`) when they hold equal values in the same order. They can therefore be used as `Hash` keys or in a `Set`:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
a = Lifo::ImmutableStack.new.push(1).push(2)
|
|
295
|
+
b = Lifo::ImmutableStack.new.push(1).push(2)
|
|
296
|
+
|
|
297
|
+
a == b # => true
|
|
298
|
+
a.equal?(b) # => false (two distinct objects)
|
|
299
|
+
{ a => :found }[b] # => :found
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Hashing looks at every value, and so does comparing two stacks of the same size, so both take time proportional to the size of the stack. Stacks of different sizes are told apart immediately. A `Lifo::ImmutableStack` is never equal to a `Lifo::Stack`, even if they hold the same values.
|
|
303
|
+
|
|
304
|
+
#### A caveat: the values themselves
|
|
305
|
+
|
|
306
|
+
The stack itself is frozen, but the values you put into it are not copied or frozen. If you push a mutable object, such as an `Array` or a `String`, changing that object afterwards changes what the stack contains:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
list = [1]
|
|
310
|
+
stack = Lifo::ImmutableStack.new.push(list)
|
|
311
|
+
|
|
312
|
+
list << 2
|
|
313
|
+
stack.peek # => [1, 2]
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
To be fully immutable, push frozen values (for example `list.freeze`, or `"text".freeze`) or values that are immutable anyway, like numbers and symbols.
|
|
317
|
+
|
|
318
|
+
## Development
|
|
319
|
+
|
|
320
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run everything CI runs:
|
|
321
|
+
|
|
322
|
+
| Task | What it does |
|
|
323
|
+
| ------------------- | --------------------------------------------- |
|
|
324
|
+
| `rake test` | Runs the Minitest suite |
|
|
325
|
+
| `rake rubocop` | Checks the code style |
|
|
326
|
+
| `rake rbs:validate` | Validates the type signatures in `sig/` |
|
|
327
|
+
| `rake yard` | Builds the API docs and fails on doc warnings |
|
|
328
|
+
|
|
329
|
+
When you change a public method, update its YARD comment in `lib/` and its signature in `sig/` along with the code.
|
|
330
|
+
|
|
331
|
+
To experiment with the code, run `bin/console` for an interactive prompt.
|
|
332
|
+
|
|
333
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
334
|
+
|
|
335
|
+
## Contributing
|
|
336
|
+
|
|
337
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/leoarnold/lifo. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/leoarnold/lifo/blob/main/CODE_OF_CONDUCT.md).
|
|
338
|
+
|
|
339
|
+
## License
|
|
340
|
+
|
|
341
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
342
|
+
|
|
343
|
+
## Code of Conduct
|
|
344
|
+
|
|
345
|
+
Everyone interacting in the Lifo project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/leoarnold/lifo/blob/main/CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lifo
|
|
4
|
+
# A persistent last-in-first-out stack backed by a singly linked list.
|
|
5
|
+
#
|
|
6
|
+
# Instances are never modified. {#push} and {#pop} return a new stack that
|
|
7
|
+
# shares its tail with the original, so both run in constant time and
|
|
8
|
+
# older versions of the stack remain valid.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# stack = Lifo::ImmutableStack.new.push(1).push(2)
|
|
12
|
+
# stack.peek #=> 2
|
|
13
|
+
# stack.pop.peek #=> 1
|
|
14
|
+
# stack.size #=> 2
|
|
15
|
+
class ImmutableStack
|
|
16
|
+
include Enumerable
|
|
17
|
+
|
|
18
|
+
# A single layer of the stack.
|
|
19
|
+
# @api private
|
|
20
|
+
Layer = Data.define(:value, :beneath, :size)
|
|
21
|
+
private_constant :Layer
|
|
22
|
+
|
|
23
|
+
# Creates an empty stack.
|
|
24
|
+
#
|
|
25
|
+
# @param top [Layer, nil] the top layer
|
|
26
|
+
# @api private
|
|
27
|
+
def initialize(top = nil)
|
|
28
|
+
@top = top
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Integer] the number of elements
|
|
33
|
+
def size
|
|
34
|
+
@top&.size || 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Boolean] whether the stack has no elements
|
|
38
|
+
def empty?
|
|
39
|
+
@top.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns a new stack with +value+ on top.
|
|
43
|
+
#
|
|
44
|
+
# @param value [Object]
|
|
45
|
+
# @return [ImmutableStack]
|
|
46
|
+
def push(value)
|
|
47
|
+
self.class.new(Layer.new(value, @top, size + 1))
|
|
48
|
+
end
|
|
49
|
+
alias << push
|
|
50
|
+
|
|
51
|
+
# Returns a new stack with all +values+ pushed in iteration order,
|
|
52
|
+
# so the last value ends up on top.
|
|
53
|
+
#
|
|
54
|
+
# @param values [Enumerable]
|
|
55
|
+
# @return [ImmutableStack]
|
|
56
|
+
def push_all(values)
|
|
57
|
+
values.reduce(self) { |stack, value| stack.push(value) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns a new stack with all +values+ pushed in reverse iteration order,
|
|
61
|
+
# so the first value ends up on top.
|
|
62
|
+
#
|
|
63
|
+
# The enumerable must be finite, since it is traversed backwards.
|
|
64
|
+
#
|
|
65
|
+
# @param values [Enumerable]
|
|
66
|
+
# @return [ImmutableStack]
|
|
67
|
+
def push_all_reverse(values)
|
|
68
|
+
values.reverse_each.reduce(self) { |stack, value| stack.push(value) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns a new stack without the top element.
|
|
72
|
+
#
|
|
73
|
+
# @return [ImmutableStack]
|
|
74
|
+
# @raise [EmptyStackError] if the stack is empty
|
|
75
|
+
def pop
|
|
76
|
+
raise EmptyStackError, "cannot pop from an empty stack" if @top.nil?
|
|
77
|
+
|
|
78
|
+
self.class.new(@top.beneath)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the top element without removing it.
|
|
82
|
+
#
|
|
83
|
+
# @return [Object]
|
|
84
|
+
# @raise [EmptyStackError] if the stack is empty
|
|
85
|
+
def peek
|
|
86
|
+
raise EmptyStackError, "cannot peek into an empty stack" if @top.nil?
|
|
87
|
+
|
|
88
|
+
@top.value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Yields each element from top to bottom.
|
|
92
|
+
#
|
|
93
|
+
# @overload each
|
|
94
|
+
# @return [Enumerator] an enumerator over the elements, top first
|
|
95
|
+
# @overload each
|
|
96
|
+
# @yieldparam value [Object]
|
|
97
|
+
# @yieldreturn [void]
|
|
98
|
+
# @return [self]
|
|
99
|
+
def each
|
|
100
|
+
return enum_for(:each) { size } unless block_given?
|
|
101
|
+
|
|
102
|
+
layer = @top
|
|
103
|
+
while layer
|
|
104
|
+
yield layer.value
|
|
105
|
+
layer = layer.beneath
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Boolean] whether both stacks hold equal elements in the same order
|
|
112
|
+
def ==(other)
|
|
113
|
+
other.is_a?(ImmutableStack) && size == other.size && to_a == other.to_a
|
|
114
|
+
end
|
|
115
|
+
alias eql? ==
|
|
116
|
+
|
|
117
|
+
# @return [Integer]
|
|
118
|
+
def hash
|
|
119
|
+
[self.class, to_a].hash
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [String]
|
|
123
|
+
def inspect
|
|
124
|
+
"#<#{self.class.name} #{to_a.inspect} (top first)>"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/lifo/stack.rb
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lifo
|
|
4
|
+
# A mutable last-in-first-out stack backed by an +Array+.
|
|
5
|
+
#
|
|
6
|
+
# {#push} and {#pop} modify the stack in place and run in amortized
|
|
7
|
+
# constant time. The top of the stack is the end of the array.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# stack = Lifo::Stack.new(1, 2)
|
|
11
|
+
# stack.peek #=> 2
|
|
12
|
+
# stack.pop #=> 2
|
|
13
|
+
# stack.size #=> 1
|
|
14
|
+
class Stack
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
17
|
+
# Marks an omitted argument, so that an explicit +nil+ can be rejected.
|
|
18
|
+
# @api private
|
|
19
|
+
UNSET = Object.new.freeze
|
|
20
|
+
private_constant :UNSET
|
|
21
|
+
|
|
22
|
+
# Creates a stack, optionally pushing +values+ like {#push_all}, so the
|
|
23
|
+
# last value ends up on top.
|
|
24
|
+
#
|
|
25
|
+
# @param values [Array<Object>] the initial elements, bottom first
|
|
26
|
+
def initialize(*values)
|
|
27
|
+
@items = []
|
|
28
|
+
push_all(values)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param source [Stack] the stack being copied
|
|
32
|
+
# @api private
|
|
33
|
+
def initialize_copy(source)
|
|
34
|
+
super
|
|
35
|
+
@items = @items.dup
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Integer] the number of elements
|
|
39
|
+
def size
|
|
40
|
+
@items.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Boolean] whether the stack has no elements
|
|
44
|
+
def empty?
|
|
45
|
+
@items.empty?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Puts +value+ on top of the stack.
|
|
49
|
+
#
|
|
50
|
+
# @param value [Object]
|
|
51
|
+
# @return [self]
|
|
52
|
+
def push(value)
|
|
53
|
+
@items.push(value)
|
|
54
|
+
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
alias << push
|
|
58
|
+
|
|
59
|
+
# Pushes all +values+ in iteration order, so the last value ends up on top.
|
|
60
|
+
#
|
|
61
|
+
# @param values [Enumerable]
|
|
62
|
+
# @return [self]
|
|
63
|
+
def push_all(values)
|
|
64
|
+
values.each { |value| push(value) }
|
|
65
|
+
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Pushes all +values+ in reverse iteration order, so the first value ends
|
|
70
|
+
# up on top.
|
|
71
|
+
#
|
|
72
|
+
# The enumerable must be finite, since it is traversed backwards.
|
|
73
|
+
#
|
|
74
|
+
# @param values [Enumerable]
|
|
75
|
+
# @return [self]
|
|
76
|
+
def push_all_reverse(values)
|
|
77
|
+
values.reverse_each { |value| push(value) }
|
|
78
|
+
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Removes the top element and returns it, or removes the top +count+
|
|
83
|
+
# elements and returns them as an array.
|
|
84
|
+
#
|
|
85
|
+
# Like +Array#pop+, this never raises for an empty stack: without +count+
|
|
86
|
+
# it returns +nil+, and with +count+ it returns what is there (an empty
|
|
87
|
+
# array for an empty stack). The array lists the elements bottom to top, with the former top
|
|
88
|
+
# element last, exactly as +Array#pop+ does. Passing it to {#push_all}
|
|
89
|
+
# therefore restores the stack.
|
|
90
|
+
#
|
|
91
|
+
# @overload pop
|
|
92
|
+
# @return [Object, nil] the removed element, or +nil+ if the stack is empty
|
|
93
|
+
# @overload pop(count)
|
|
94
|
+
# @param count [Integer] how many elements to remove
|
|
95
|
+
# @return [Array<Object>] the removed elements, top last
|
|
96
|
+
# @raise [ArgumentError] if +count+ is not a non-negative +Integer+
|
|
97
|
+
def pop(count = UNSET)
|
|
98
|
+
return @items.pop if count.equal?(UNSET)
|
|
99
|
+
|
|
100
|
+
unless count.is_a?(Integer) && count >= 0
|
|
101
|
+
raise ArgumentError, "Expected a non-negative Integer, got: #{count.inspect}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@items.pop(count)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns the top element without removing it.
|
|
108
|
+
#
|
|
109
|
+
# @return [Object]
|
|
110
|
+
# @raise [EmptyStackError] if the stack is empty
|
|
111
|
+
def peek
|
|
112
|
+
raise EmptyStackError, "cannot peek into an empty stack" if empty?
|
|
113
|
+
|
|
114
|
+
@items.last
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Removes all elements.
|
|
118
|
+
#
|
|
119
|
+
# @return [self]
|
|
120
|
+
def clear
|
|
121
|
+
@items.clear
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Yields each element from top to bottom.
|
|
126
|
+
#
|
|
127
|
+
# @overload each
|
|
128
|
+
# @return [Enumerator] an enumerator over the elements, top first
|
|
129
|
+
# @overload each
|
|
130
|
+
# @yieldparam value [Object]
|
|
131
|
+
# @yieldreturn [void]
|
|
132
|
+
# @return [self]
|
|
133
|
+
def each(&)
|
|
134
|
+
return enum_for(:each) { size } unless block_given?
|
|
135
|
+
|
|
136
|
+
@items.reverse_each(&)
|
|
137
|
+
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Boolean] whether both stacks hold equal elements in the same order
|
|
142
|
+
def ==(other)
|
|
143
|
+
other.is_a?(Stack) && to_a == other.to_a
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @return [String]
|
|
147
|
+
def inspect
|
|
148
|
+
"#<#{self.class.name} #{to_a.inspect} (top first)>"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/lifo/version.rb
ADDED
data/lib/lifo.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lifo/version"
|
|
4
|
+
|
|
5
|
+
# Last in, first out: mutable and immutable stacks in pure Ruby.
|
|
6
|
+
#
|
|
7
|
+
# @see Lifo::Stack
|
|
8
|
+
# @see Lifo::ImmutableStack
|
|
9
|
+
module Lifo
|
|
10
|
+
# Base class of all errors raised by this library.
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Raised when reading from or popping an empty stack.
|
|
14
|
+
class EmptyStackError < Error; end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require_relative "lifo/immutable_stack"
|
|
18
|
+
require_relative "lifo/stack"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Lifo
|
|
2
|
+
class ImmutableStack[T]
|
|
3
|
+
include Enumerable[T]
|
|
4
|
+
|
|
5
|
+
@top: Layer[T]?
|
|
6
|
+
|
|
7
|
+
class Layer[T] < Data
|
|
8
|
+
attr_reader value: T
|
|
9
|
+
attr_reader beneath: Layer[T]?
|
|
10
|
+
attr_reader size: Integer
|
|
11
|
+
|
|
12
|
+
def self.new: [T] (T value, Layer[T]? beneath, Integer size) -> Layer[T]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize: (?Layer[T]? top) -> void
|
|
16
|
+
def size: () -> Integer
|
|
17
|
+
def empty?: () -> bool
|
|
18
|
+
def push: (T value) -> ImmutableStack[T]
|
|
19
|
+
def <<: (T value) -> ImmutableStack[T]
|
|
20
|
+
def push_all: (Enumerable[T] values) -> ImmutableStack[T]
|
|
21
|
+
def push_all_reverse: (Enumerable[T] values) -> ImmutableStack[T]
|
|
22
|
+
def pop: () -> ImmutableStack[T]
|
|
23
|
+
def peek: () -> T
|
|
24
|
+
def each: () { (T value) -> void } -> self
|
|
25
|
+
| () -> Enumerator[T, self]
|
|
26
|
+
def ==: (untyped other) -> bool
|
|
27
|
+
def eql?: (untyped other) -> bool
|
|
28
|
+
def hash: () -> Integer
|
|
29
|
+
def inspect: () -> String
|
|
30
|
+
end
|
|
31
|
+
end
|
data/sig/lifo/stack.rbs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Lifo
|
|
2
|
+
class Stack[T]
|
|
3
|
+
include Enumerable[T]
|
|
4
|
+
|
|
5
|
+
UNSET: Object
|
|
6
|
+
|
|
7
|
+
@items: Array[T]
|
|
8
|
+
|
|
9
|
+
def initialize: (*T values) -> void
|
|
10
|
+
def initialize_copy: (Stack[T] source) -> void
|
|
11
|
+
def size: () -> Integer
|
|
12
|
+
def empty?: () -> bool
|
|
13
|
+
def push: (T value) -> self
|
|
14
|
+
def <<: (T value) -> self
|
|
15
|
+
def push_all: (Enumerable[T] values) -> self
|
|
16
|
+
def push_all_reverse: (Enumerable[T] values) -> self
|
|
17
|
+
def pop: () -> T?
|
|
18
|
+
# Accepts any object so that invalid counts raise ArgumentError.
|
|
19
|
+
| (untyped count) -> Array[T]
|
|
20
|
+
def peek: () -> T
|
|
21
|
+
def clear: () -> self
|
|
22
|
+
def each: () { (T value) -> void } -> self
|
|
23
|
+
| () -> Enumerator[T, self]
|
|
24
|
+
def ==: (untyped other) -> bool
|
|
25
|
+
def inspect: () -> String
|
|
26
|
+
end
|
|
27
|
+
end
|
data/sig/lifo.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lifo
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Leo Arnold
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: The stack. As seen on COMPSCI 61B. A data structure so useful and ubiquitous
|
|
13
|
+
that you'd think it was part of the Ruby standard library. Turns out it isn't, so
|
|
14
|
+
here you go ...
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- CHANGELOG.md
|
|
20
|
+
- CODE_OF_CONDUCT.md
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/lifo.rb
|
|
24
|
+
- lib/lifo/immutable_stack.rb
|
|
25
|
+
- lib/lifo/stack.rb
|
|
26
|
+
- lib/lifo/version.rb
|
|
27
|
+
- sig/lifo.rbs
|
|
28
|
+
- sig/lifo/immutable_stack.rbs
|
|
29
|
+
- sig/lifo/stack.rbs
|
|
30
|
+
- sig/lifo/version.rbs
|
|
31
|
+
homepage: https://github.com/leoarnold/lifo
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
allowed_push_host: https://rubygems.org
|
|
36
|
+
changelog_uri: https://github.com/leoarnold/lifo/blob/main/CHANGELOG.md
|
|
37
|
+
rubygems_mfa_required: 'true'
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 3.3.0
|
|
46
|
+
- - "<"
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '5'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 4.0.16
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: 'Last in, first out: implementations of mutable and immutable stacks in pure
|
|
58
|
+
Ruby'
|
|
59
|
+
test_files: []
|