Part 3. Implementing some methods (55 points)
At this point, you should have some code that can parse the fields of the
index file and extract the author, title, and URL fields and stick them in an
Entry
.
In this part, you’re going to create an Index
structure which will hold all
of the Entry
s and implement some methods on it to perform a simple search.
Recall that to implement methods for a structure, we use an impl
block.
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct Example { name: String, url: Option<String>, } impl Example { fn new(name: String) -> Self { Self { name, url: None } } fn has_url(&self) -> bool { self.url.is_some() } fn url(&self) -> Option<String> { // Return a copy of the url member. self.url.clone() } fn set_url(&mut self, url: &str) { self.url = Some(url.to_string()); } fn into_name(self) -> String { self.name } } }
Some things to notice here:
- There’s a
new()
function which returns aSelf
. Inside animpl
block,Self
refers to the type being implemented, hereExample
; - The
new()
function does not takeself
,&self
, or&mut self
as an argument because this isn’t a method you call on an instance of anExample
. Instead, you’d call it to create an instance. This is similar to a constructor in Java although it’s just convention and not inforced. To callnew()
, we useExample::new("Blarg".to_string())
. It’s a good practice to have anew()
function (which may or may not take arguments as the situation demands) and returns one ofSelf
,Option<Self>
orResult<Self, SomeErrorType>
. The last two are used when it’s possible for thenew()
function to fail. - The
has_url()
andurl()
methods take&self
as the first argument. This is similar to Python’sself
argument and Java’s implicitthis
argument in methods; - The
set_url()
method takes&mut self
rather than&self
. This is necessary because modification toself
requires that it be mutable. If you remove themut
from the method signature, it’ll fail to compile; and - The
into_name()
method takesself
rather than&self
. This method takes ownership of theExample
object (meaning it cannot be used after calling.into_name()
) and returns thename
field without making a copy (as theurl()
method does).
Your task
Define a new structure called Index
which will hold all of the Entry
s
you’re going to parse from the index file. You’re going to implement the following methods.
#![allow(unused)] fn main() { type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; struct Entry; #[derive(Debug, Clone)] struct Index { // TODO: What field(s) do you want here? } impl Index { /// Create a new `Index` by reading from `pgindex.txt`. fn new() -> Result<Self> { todo!() } /// Returns the number of entries in the index. fn len(&self) -> usize { todo!() } /// Returns a vector containing references to entries whose titles /// contain `search_string`. fn title_search(&self, search_string: &str) -> Vec<&Entry> { todo!() } /// Returns a vector containing references to entries whose authors /// contain `search_string`. fn author_search(&self, search_string: &str) -> Vec<&Entry> { todo!() } /// Returns a vector containing references to entries whose titles /// or authors contain `search_string`. fn search(&self, search_string: &str) -> Vec<&Entry> { todo!() } } }
To implement new()
, take your code from the run()
in the previous part and
move it into new()
. Rather than print out the entries, you’re going to need
to collect them all into some data structure and store them in your Index
structure and return that.
The len()
method should return the number of entries in the index.
The three search methods should behave similarly: Each converts the
search_string
to lowercase. Then, for each entry in the index, convert the
relevant fields (title, author, or both) to lowercase. If the lowercased
search_string
is contained in the field, add a reference to the
corresponding entry to a results
vector and return that.
You’ll probably want something like this.
fn title_search(&self, search_string: &str) -> Vec<&Entry> {
let mut results = Vec::new();
for entry in ??? {
if ??? {
results.push(entry);
}
}
results
}
The other two are similar; however, remember that some entries don’t have authors. You can use
if let Some(ref author) = entry.author {
// This entry did have an author and `author` is currently a reference
// to the `String` stored inside the `Option<String>` that is the
// `entry.author` field.
}
If you omit the ref
in if let Some(ref author)
, you’ll get an error about
trying to move out of entry
which isn’t mutable so you cannot. ref
tells
Rust that you want reference instead.
Once you have implemented one of these functions, it’s worth testing your
code. If you change your run()
function to
fn run() -> Result<()> {
let index = Index::new()?;
println!("There are {} entries in the index", index.len());
let results = index.title_search("the inferno");
println!("{results:#?}");
Ok(())
}
you should see three results, The Inferno by Barbusse and O’Brien, The Divine Comedy of Dante Alighieri: The Inferno by Dante, and The Inferno by Strindberg.
In the remaining parts of the lab, you’re going to handle parsing the
resources, implementing the Display
trait to make printing better, and
finally the simple user interface shown in the lab’s overview.