Lesson 5
Managing Document Editing History using Stacks in Go
Introduction

Welcome! Today, we will delve into managing a document's editing history using stacks. Imagine building a text editor; you would need to handle actions like adding text, undoing, and redoing those changes. We will see how these features can be efficiently implemented using stacks. By the end of this lesson, you will possess an in-depth understanding of applying stacks in practical scenarios.

Introducing Methods to Implement

Before starting the coding portion, let's dissect the functions we will implement. These functions will manage a document's edit history, allowing us to apply changes, undo them, and redo them effectively.

  • func (d *DocumentHistory) ApplyChange(change string): This function applies a change to the document. The change, represented as a string, is stored in a way that allows us to remember the order of applied changes. Any previously undone changes are discarded.

  • func (d *DocumentHistory) Undo() (string, bool): This function undoes the most recent change and allows us to store it for a possible redo. It returns the change that was undone and a boolean indicating success. If there are no changes available to undo, it returns an empty string and false.

  • func (d *DocumentHistory) Redo() (string, bool): This function redoes the most recent undone change, making it active again. It returns the change that was redone and a boolean indicating success. If there are no changes available to redo, it returns an empty string and false.

  • func (d *DocumentHistory) GetChanges() []string: This function returns a list of all applied changes in the order they were applied.

Methods Implementation with Stacks

To implement these functions efficiently, we'll use two slices to simulate stack behavior. Slices in Go allow dynamic resizing and efficient element insertion/removal from the end. This LIFO (Last In, First Out) property is ideal for keeping track of applied changes and undone changes. The most recent change is always at the end, making it easy to undo and redo.

Let's break down each function's implementation and study how they interact with the slices.

Step 1: Defining the Struct

Let's implement the solution step by step. First, we define our struct and set up the initial state.

Go
1package main 2 3type DocumentHistory struct { 4 changesStack []string 5 redoStack []string 6}

Here, DocumentHistory is the struct, and we initialize two slices: changesStack to act as our stack of applied changes and redoStack for undone changes.

Step 2: Implementing the ApplyChange Function

Next, we implement the function to apply changes.

Go
1// ... previous code 2 3func (d *DocumentHistory) ApplyChange(change string) { 4 d.changesStack = append(d.changesStack, change) 5 d.redoStack = d.redoStack[:0] // clear the redo stack 6}

With ApplyChange, we take a string change and append it to the changesStack slice. Additionally, we clear the redoStack to ensure that once a new change is applied, any previously undone changes cannot be redone.

Step 3: Implementing the Undo Function

Now, we will implement the function to undo the most recent change.

Go
1// ... previous code 2 3func (d *DocumentHistory) Undo() (string, bool) { 4 if len(d.changesStack) == 0 { 5 return "", false 6 } 7 change := d.changesStack[len(d.changesStack)-1] 8 d.changesStack = d.changesStack[:len(d.changesStack)-1] 9 d.redoStack = append(d.redoStack, change) 10 return change, true 11}

Here, Undo checks if the changesStack slice is empty. If it is, it returns an empty string and false. Otherwise, it pops the last item from the slice, simulating a stack pop operation, pushes it to the redoStack, and returns the undone change with true.

Step 4: Implementing the Redo Function

Now we'll create the function to redo the most recent undone change.

Go
1// ... previous code 2 3func (d *DocumentHistory) Redo() (string, bool) { 4 if len(d.redoStack) == 0 { 5 return "", false 6 } 7 change := d.redoStack[len(d.redoStack)-1] 8 d.redoStack = d.redoStack[:len(d.redoStack)-1] 9 d.changesStack = append(d.changesStack, change) 10 return change, true 11}

The Redo function checks if the redoStack is empty. If it is, it returns an empty string and false. Otherwise, it pops the last item from the slice, simulating a LIFO operation, pushes it back to the changesStack, and returns the redone change with true.

Step 5: Implementing the GetChanges Function

Lastly, we implement the function to retrieve all applied changes.

Go
1// ... previous code 2 3func (d *DocumentHistory) GetChanges() []string { 4 return append([]string(nil), d.changesStack...) 5}

The GetChanges function returns a copy of the changesStack slice, which includes all changes applied to the document in the order they were applied. We ensure it's a copy by appending to a new, empty slice.

Summary

In today's lesson, we learned how to manage a document's editing history using stacks. We implemented functions to apply changes, undo the last change, redo the most recent undone change, and retrieve all applied changes using Go's slices for stack behavior. This exercise provided us with practical experience in using slices to efficiently track and revert operations in a real-life scenario. It's crucial to understand how Go's handling of slices and errors differs from other languages, which emphasizes multiple return values. Keep practicing similar challenges to deepen your understanding of data structures in Go. Fantastic job today, and keep up the good work!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.