- Applications often need to read and write data to a file.
For example, our application reads
and writes a file containing CSV data.
Each line, as we've seen in the file,
has this kind of structure,
the startDate, endDate, description.
So our application reads one line at a time,
and each line it puts it into a Visit structure.
We've seen a little bit about this already.
The Visit structure contains the start date
and the end date and the description as fields
as read in from the file.
So I guess the first thing we should do is
to take a look at the Visit structure in types.rs.
So here we are in types.rs,
amongst all the other details,
we have the Visit structure.
It stores the start date as a NaiveDate.
Remember, NaiveDate represents a date
without time zone information.
That's why it's naive.
It's not that naive really.
It's a date object without timestamp.
The start_date and the end_date and the description.
Couple of points to note here,
from a coding quality point of view,
the structure is public, because obviously,
I'm gonna be using that structure extensively
in the application.
But do you notice that the fields aren't public?
This is encapsulation.
These fields aren't public.
In other words, they're private.
They can only be accessed in the code in this file,
and that's proper encapsulation.
It would be the equivalent of declaring private fields
in a class in Java or C# or C++.
The other point I wanna make while I'm here
is the NaiveDate structure type
is defined in the chrono crate.
Whenever you've got a crate that starts, that isn't std,
then you have to basically add this
as a dependency into your TOML file.
The standard library is standard and it's already included,
but any additional crates you've gotta specify, like chrono,
you've gotta add into your dependencies.
So in Cargo.toml, I've added chrono as a dependency.
So when the application runs the first time
or when you build it, it'll have to fetch.
Well, fetch the latest version of chrono
or a version number that you specify
into your current application folder.
And then once you've done that,
you can then use that chrono package.
So let's go back into types.rs.
Here is my Visit structure containing the start_date,
the end_date, and the description.
So I create one
of those objects every time I read another line
of text from the file.
So let's take a look
at how we can read CSV data from a file.
We'll have a look at fh.rs.
That's our file handling module.
And there's a function read_visits_from_file.
Okay, let's take a look at that.
So in fh,
here we are.
It's a public function, read_visits_from_file.
It takes the name of the file like data.txt,
and it returns a vector of Visit,
where Visit is defined in the types module.
Remember, what the crate keyword means in Rust?
Generally, your application is gonna have lots of modules.
At the very top level in your application,
right at the top of your crate,
it defines a module called types, okay?
So technically the crate or the main code I should say
is at the root namespace in your application,
and types is like a sub namespace, okay?
So underneath your crate,
you've created effectively a namespace called types.
In order to use that module or that namespace elsewhere,
like in file handling,
we say crate is a shorthand for the root folder
or the root namespace in our application,
go to the top level namespace of our project,
and then give me the types sub namespace, okay?
So from the top, give me access to the types namespace.
What I could have done is I could have gone further
and said ::Visit, okay?
And in that case, this name would be in scope directly.
And then I wouldn't have to say types::Visit.
I could just say Visit,
and Visit, like that.
So depends which you prefer.
Sometimes it's actually,
sometimes I find it preferable
to actually include the module name as a prefix
and not to import the actual structure directly.
So it's this name now that's in scope.
And it's that name you have to use as a qualifier
to say in that module, give me the Visit type, okay?
So you can see that the Visit type came from that module
and then you know where to look to find it.
Anyway, one way or the other,
the read_visits_from_file function returns a vector
of Visit objects.
So let's take a look in here.
Notice that there are two different modules involved here.
In the std::fs, that's the standard file system module,
there's a File structure.
And then in the std::io module,
there are lower level structures and traits
that I'm gonna describe now.
So we are talking about how to read from a file, okay?
So obviously at this point it's the BufReader.
BufReader is a structure, and BufRead is a trait,
and you'll see why in a moment.
So first of all, you say File::open, okay?
So from the std::fs::File,
File::open, give it the file name.
And that returns a result, a result enum,
could be okay, could be an error.
If I was in any doubt about this working,
I would have a match statement
to say match okay, match error.
I'm not gonna bother doing that,
I'm just gonna unwrap it, okay?
If there was any possibility
that this could actually fail,
then I'd put that into a match statement instead,
but I'm fairly confident that's gonna work.
And it gives you back a file handle basically, here.
And then what you do is you pass that file handle
into a BufReader.
Lots of languages have a similar kind of API.
BufReader, that'll create a new BufReader,
which can read lines of text from that file.
So here is my BufReader object, right?
Then, oh, I create a vector of Visits.
I'm gonna be adding Visit objects into that vector.
Every time I read a line of text,
I'll convert it into a Visit object,
and I'll add it into this vector.
So take in my BufReader that I created, you can iterate.
It has a function called lines, which is an iterator.
Basically it'll give you back one line at a time, okay?
So it'll give you back a line.
And that line, I unwrap it, okay,
to give me back a string, it could issue a result.
So I unwrap it, that'll give me back a string.
And then you know like start_date, end_date, description.
And I pass that string
into a function in my Visit structure,
a factory function,
which will create a Visit from a string.
It's kind of like a parse string function.
It parses the string, it grabs the start_date, the end_date,
and the description, and it puts it into a Visit object.
And then it returns it.
We'll have a look at that function a bit later.
I wanna avoid all the date stuff just for now.
We'll get back to that later.
It creates a Visit object based
on the line of text that we just read in.
And then we push that Visit into my vector of all Visits.
And at the end, I just return all the Visits.
Remember in Rust, a function that ends
in an expression is like a return statement.
So I could have said return Visits,
and that would've been fine.
You might prefer to write it that way
or a rusty crustacean would write it that way.
Now this one particular point I wanna mention here,
in the code we've just looked at,
you can see that the File structure is used.
So you can see why I've imported that.
And you can also see the BufReader structure
is being used, so you can see why I'm using that.
But no matter how hard you look in the code here,
it doesn't actually seem to use BufRead.
What on earth is BufRead?
Maybe it was a mistake, maybe I could just delete it, okay?
Well, it wasn't a mistake.
Look, see what happens if I try to compile the code now.
This is quite subtle, but it's a kind
of mistake you will doubtless make at some point.
I have for sure.
Right.
So this BufReader, okay, which has a lines method,
it's saying this BufReader doesn't have a lines method,
but what's going on here?
How is that even work?
And I tell you what the answer is.
We looked at this when we talked about traits.
So the first fact is BufReader
implements a trait called BufRead, okay?
And I can show you the documentation for that.
BufReader.
The names of these things aren't great in the library.
BufReader implements the BufRead trait.
Let me show the documentation to prove that point.
Here's the documentation for the BufReader structure
in the std::io module.
You can see that BufReader does implement the BufRead trait.
Okay, so what?
Let's have a look at the BufRead trait.
If you skim down, this is the implementation
of the BufRead trait for BufReader.
If you scroll down just a little bit, you'll notice
that the lines method is actually part of the BufRead trait.
The BufReader structure implements this trait,
therefore, it implements the lines method.
But the thing is, if you in your code,
in your outside code, if you invoke a method,
which is implemented via a trait on that structure,
not only do you have
to include the structure type in your code,
you've also gotta include the trait as well, okay?
Even though you're not using the trait yourself,
you're calling the lines method.
And the lines method is included in BufRead, okay?
It's part of the BufReader implementation.
So that's a surprise for most developers
in other languages.
I've gotta actually say if BufReader implements BufRead,
and if I call any of the methods
that are actually defined in there,
then I have to include that trait
in my list of imports as well.
I can't just include the BufReader trait,
the BufReader structure.
I've gotta also include
or import the BufRead trait that it implements.
So when I call the lines method,
the compiler basically has to know
that the lines method complies with what's defined in there.
So import the structure,
plus if you use any methods in that structure,
which come from a trait,
then you have to import the trait as well.
A very subtle point there.
That's the sort of thing,
you're gonna scratch your head over for a while.
I certainly did when I was learning Rust myself.
So let me just do a quick cargo check to make sure
that I've reinstated the code in its spit state,
cargo check.
Now it's good.
So the next thing we're going to look at is
to write CSV data to a file.
It's in the same source file.
It's the write_visits_to_file function.
Let's a look at that now.
Okay, so write_visits_to_file.
This is more straightforward actually.
It takes the file name, data.txt.
It takes the vector of Visits
that it needs to write to a file.
So in the file structure in the standard library,
there's an associated function, a static function basically.
It creates a new file, that returns a result enum,
which could be okay or it could be an error.
So I'm assuming it's gonna be okay.
And I just unwrap the object.
If it had been an error,
my program would've crashed at that point.
It would've panicked.
If I had any doubt at all that it could crash,
then I wouldn't just unwrap it,
I'd say match okay or error.
Anyway, it gives you back a file handle.
And in a similar kind of way,
you create a BufWriter upon that file.
Here's my BufWriter object, called writer.
I iterate through each of my Visits.
Visits is just my vector.
I iterate and get back each Visit structure,
a reference to it,
and then I write each structure to file.
Using my BufWriter, I call the write_all method.
By the way, the write_all method is defined in this trait.
So BufWriter implements Write.
And when you call it a method defined in the Write trait,
you've gotta include the Write trait in your code.
If I didn't include that, I'd get the same kind
of error I had when I didn't include that, okay?
So a structure which implements a trait,
if you call any of those methods from the trait,
you have to include that trait as well.
And anyway, we write all the Visits.
So here we go, Visit is an object.
As we'll see later on,
it's got a two string function
and I just converted to bytes, okay?
The file writer is expecting bytes not characters,
because characters are quite tricky in Rust.
A character could be one byte, like the letter H,
or it could be four bytes, like a smiley face.
So we have to convert it
into primitive bytes that can then be written to the file.
Not surprisingly, whenever you're doing file handling,
it could fail.
And remember, the way that Rust indicates possible failure
is via the result enum.
It doesn't throw an exception,
it just returns a result, which may be an error.
So I've just unwrapped the result
in the hope and the assumption that it's okay.
And again, in like proper code,
you wouldn't just unwrap it, you'd have a match.
Match okay? Okay, no problem.
Match error? That's an exception basically.
And then you have to do some recovery process.
So looking quite a lot there actually,
mostly to do with file handling,
and a reminder about the weird world of Rust structures
and how it implements traits.