How I implemented a tariff billing terminal app using rust

Isaac Ssemugenyi
8 min readDec 13, 2023
A piece of code from the rust application in this article

In my continued journey of learning rust and using it in my personal and hobby projects. Today I am writing about power tariff billing app that I first wrote in C as a course assignment and later decided to write a rust version of the application too. So below is my experience of re-writing the app in rust. I tell you it was much smoother writing it in rust than it was in C.

The project was written using cargo but no external crate was used and among the features I tried out were but not limited to:-

  1. Working with modules
  2. Generating random numbers (not using the rand crate)
  3. Creating dynamic folders (this was tested on a mac system)
  4. Checking if a folder exists
  5. Creating dynamic files
  6. Writing to files using the write! and format! marcos
  7. Reading from files
  8. Using structs, arrays, and other data structures
  9. Fighting with the borrow-checker(but the editor and the rust compiler helped a lot here)
  10. Using a lot of “match” instead of if statements (need some feedback on this whether it is a better practice)

In this write up we shall talk about each of these features briefly and later link to the github repo with the project.

Working with modules

I found working with modules a bit complicated that I had hoped as illustrated in the gist below, in the main.rs it is as easy as using mod <module_name>; while in other files, first I failed to access the functions in other modules until Intellisense suggested use crate::{<module_name>, <function>}; . If you can highlight why this is so, please put it in the comment.

Generating random numbers

During the development process of this application there was a need to generate random numbers, these were used to generate tariff meter numbers and tariff receipt numbers for each user. I did some research and found out that most of the resources where recommending the rand crate but I wanted to use bare rust without an external crate. To solve this issue I came across an implementation on users.rust-lang.org posted by mgeisler that did the job.

In the above snippet when the function is invoked, it looks at the current system time, gets the time, expresses it as duration since 1970 and returns the milliseconds that have elapsed since 1970, I also return a txt file name expressed as <milliseconds>.txt and this is a personal preference returning a tuple.

This can be considered a bad design, but I got lazy as I first used the function to generate file name and only returned filenames, later when I added the meter number generation, I used the same function, hence adding the u32 too into the tuple.

Creating dynamic folders

This functionality was needed given the application is dynamic, when a user selects that there are new customers, the application dynamically creates a folder (the customer meter number) and it is within this folder that all the generated receipts for this customer will be saved.

Here I use the create_dir which is imported from std::fs.

Note: this functionality was only tested on the a mac but from the documentation rust says this functionality is cross-platform.

create_dir is imported from std::fs

Checking if a folder exists

This functionality comes in handy when checking if a meter/ account number exists. This is used when a customer selects that they are an existing customer, the program asks them for their meter number, which directly translates to a folder in the file system.

Here we use the fs::metadata to confirm if a folder/file exists or not, which when combined with is_dir(), the result of metadata returns either a true if a folder/file exists or false if it does not.

In the application, I match this and print the right message to the customer.

You can read more about metadata in the official rust docs.

Creating dynamic files

This was possible with the use of the File struct from std::fs.

let mut output = File::create(filename)?;

File::create(filename)?; creates a file with the filename given and the file passed to the create is a .txt file.

Writing to files using the write! and format! marcos

In the application , there is a requirement to write to the text file created in the dynamic file section above. In the process of writing to file, there is a need to write dynamic and formatted data, as the values are always generated by the application.

It this case the write! marco is used and this is imported from std::io, the format! marco is also used to concatenate the auto generate file name and the folder (meter number) to save the user details into the right file.

let output_file = format!("{}/{}", meter_number, file_name); is used to create a single path joining the folder name and the file name.

write!(&mut output_file, "Consumer Type: {}\t\t\t|", consumer_type).expect("Failed to write to file"); is used to write to the file while applying all the formatting.

Reading from files

Reading from the file is straight forward as shown below.

Inside the read function,

  1. I attempt to open the file, using the File struct, if it fails, we raise an error.
  2. I create a mutable string variable contents
  3. I read from the file using read_to_string method and assign this to the contents string.
  4. Lastly, I print the content to the terminal.

Using structs, arrays, and other data structures

During development, I use quite a number of data structures, including structs and arrays. I use structs to define the properties that define my customers and array to store some of my tariff costs.

Fighting with the borrow-checker

If you have worked with rust, you understand this problem but the editor together with intellisense and the compiler helped to remove the blockers.

Using a lot of “match” instead of if statements

In this application I use quite a number of match statements than if statements to the extent that I have some nested match statements.

If you look at the function above, you get to see that I have a section with 3 levels of nesting using the match statement. From my understanding it works but feel free to drop a comment if you have a better way of reducing on the match statements in a program or advising how I can use them in a better way.

All this concludes the technical section. If interested in the whole program you can access it through the following github link.

Still one could be wondering why I used cargo instead of using rustup since I did not install any external crates, there is no specific reason I just choose to use cargo and I believe even without cargo the same functionality can be achieved.

Now let us look at the application functionalities.

The application consists of 4 files, main.rs, terminal_print.rs, read_file.rs, and write_file.rs . Each of which contains implementation to enable the smooth functioning of the application.

I don’t know what we call prop drilling in rust, but there was a lot of prop drilling for the folder_name or meter_number as I always needed it to save each receipt for every operation. If not this approach of passing the meter_number as a parameter to many functions in order to access it in the write_to_file function, I would have used the option of creating a global constant variable, which in rust to be able to modify it later, you need to use the unsafe block, which I did not want in this case. So I resorted to passing the folder name until it was accessed by the required function.

Step 1: To run the application, first clone a copy from this github link.

Step 2: Run cargo run from the terminal in the directory where you have cloned the project.

Step 3: Either choose 1 if you are a customer or 2 if you are not. In case of 2, you get asked whether you want to become a customer or not. If 1 is chosen, then an account will automatically be created for you, for this demo account number 514909000 was created (folder created on the file system with that number as the name).

From there a user can now continue to select which tariff they want to calculate power for.

If 1 was chosen, the user will be asked to provide their meter number (folder_name) which the system will check and if it exists, the user will be allowed to continue and select the tariff plan to calculate power for.

If 2 is selected on the first prompt of “Are you a new customer or an existing customer” and again the user selects 2 for on “Do you want to become a customer”, the application will just stop.

If a user has an account entered and chooses option 6, they will prompted to input the receipt number, which will be mapped to a text file that has the details of that receipt. If the receipt exists, it gets printed to the terminal, if not it will not be printed and will tell the user that receipt does not exist.

In the above output there is a challenge of clearly organizing the printout of the table.

Step 4: Lastly, here is an example when a given option is selected and the units entered.

A link to the github repo of the entire source code

References

https://www.umeme.co.ug

That is all there is to the application.

If you liked the application, enjoyed reading or learnt something new in rust. Please consider clapping, following me here or connecting with me on linkedin or twitter.

Until next time stay safe.

--

--

Isaac Ssemugenyi

Software Engineer with Stanbic Flyhub Uganda, Love programming with javascript, nodejs, vuejs, react, react-native and Java.