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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-09-21
4
+
5
+ - Initial release
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lifo
4
+ VERSION = "0.1.0"
5
+ end
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Lifo
2
+ VERSION: String
3
+ end
data/sig/lifo.rbs ADDED
@@ -0,0 +1,7 @@
1
+ module Lifo
2
+ class Error < StandardError
3
+ end
4
+
5
+ class EmptyStackError < Error
6
+ end
7
+ end
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: []