Temperature Converter
This will be your first small C3 project. As you can probably tell by the name, we are going to write a small program that can convert a temperature from Celsius to Fahrenheit. It will teach you a few common concepts, like variables, reading input, simple error handling and more. We will explore these concepts in greater detail later on.
Creating a New Project
We start by creating a new project using the c3c init command and we will name it appropriately.
c3c init temperature_converter
cd temperature_converterThe cd command will move you into the project’s directory, which you can now open in your code editor of choice. Make sure that the integrated terminal of your code editor is in the correct directory as well.
Opening the main.c3 file under src you should find the following code:
module hello_world;
import std::io;
fn int main(String[] args)
{
io::printn("Hello, World!");
return 0;
}Run it with c3c run and check if the output is Hello, World!. If this is the case we can now remove the first line inside of the main function so that we have an empty coding canvas for us to fill.
Reading User Input
String input = io::treadline();This is all that is needed to read input from the console in C3.
In this line we are declaring a variable of type String and give it the name: “input”.
A String is simply a sequence of characters. This can be a word, a sentence or only two letters. You can also store a single letter inside of a String, but you would usually use char (short for character) in that case.
The = sign tells our program that the value on the right side should be assigned to the variable on the left side. In our case this means that when io::treadline() is done reading the users input it will store this input into our “input” variable.
Note
The t in io::treadline() stands for temporary. It uses the temporary allocator and automatically discards allocated memory. As memory management is a more advanced topic, it will be covered later on.
Optionals
If you try running your program now it won’t compile:
Error: It is not possible to cast from 'String?' to 'String'. This is because our variable doesn’t have the correct type. The io::treadline() macro doesn’t just return a String, it returns String?. The compiler is telling you that it can’t covert (cast) the String? to a regular String.
Note
We will discuss macros in depth later, for now just think of them as functions.
In C3 error handling is done via optionals. An optional either has a value or is empty. If an optional is empty, it must provide an excuse in the form of a fault (error). Optionals are declared by adding an exclamation mark after the type. In our example the type String becomes the optional String?. String? can now contain either a string like “I’m a string!” or a be empty, in which case it must provide an excuse like io::EOF.
Add the exclamation mark at the end of our variable’s type and try running your program again. Looking at the console we can see that the program isn’t completing with an exit code. That’s because it is still running, io::treadline() will wait for input until you press the Enter key. Doing so will stop it and we should see our exit code of 0.
Unfortunately we can’t see what the “input” variable contains. Let’s change that by adding a print statement like so:
String? input = io::treadline();
io::printn(input);Try running your program now.
Understanding Errors
You will notice that the compiler is protesting again:
Error: The result of this call is optional due to its argument(s).
The optional result may not be implicitly discarded.
Consider using '(void)', '!' or '!!' to handle this.But it has good reason to do so. An error can lead to problems and unexpected issues further down in your code. Let’s look at this in action, by forcing the compiler to print out our variable. As the error message suggests we can add two exclamation marks to force the compiler to treat the optional as a normal variable (this is also called force unwrapping). Do this by adding !! after our input variable in the printn macro. This isn’t recommended and we will see why if we run the program now.
Writing some text and pressing enter will now print the “input” variable under the text that you wrote. This means that you should now have the same text twice in your console. This worked fine, because we gave a valid text as input, but what if we supply something that gives an error? Run your program again and press Ctrl + D if you are on MacOS/Linux or Ctrl + Z + Enter on Windows.
You should now be presented with a real error that crashes your program immediately. Now, what would you as a user prefer? If the program told you: “There was an error, try again!” or if it simply crashed and possibly lost 2 hours of unsaved progress.
It is clear that the first option is preferable. For that reason we will gracefully handle the error.
Handling Optionals
C3 offers multiple options for gracefully handling errors. We will be using the most common way of catching an error. The if-catch:
if (catch excuse = optional)
{
}What this essentially does is: If I catch a fault (error) in my optional, assign that fault to the “excuse” variable, then execute the code inside of the if-statement. The variable which you put behind the catch keyword doesn’t have to be called “excuse”, but “excuse” is the most common name. The variable is of type: anyfault by default. This type can, as the name suggests, hold any fault/error.
Going back to our project, we can include this if-catch like so:
String input = io::treadline();
if (catch excuse = input)
{
io::printn(excuse);
return 1;
}If “input” happens to be empty, we retrieve it’s excuse and assign it to our “excuse” variable. After this we execute the code inside of the if-statement, which prints out the “excuse” variable with io::printn(excuse); and we return an exit code of 1, which exits our program (because of the return) and tells us that there was an error.
Note
The exit code 1 is used to indicate an unsuccessful exit.
If we had a larger application we could also simply tell the user that an error occurred without exiting, so that they can stil interact with the application and save their progress for example.
You can try running the program again and use the Ctrl + D or Ctrl + Z + Enter trick. The program should now simply print the cause of the error: io::EOF and exit with the status code of 1.
Tip
EOF stands for End-of-File and signals the terminal that there is no more input available. This causes an error because you essentially told your program to listen for input and the first thing it gets is: no more input. If you write some text before pressing Ctrl + D or Ctrl + Z + Enter the program won’t error, since it received input.
Converting String to Float
We are now able to successfully read input in the form of a string, however to perform calculations we need to convert this string to a number. In C3 there are multiple types for numbers. Let’s take a look at the two most common ones:
int number = 10;
float number = 10.3;Note
This code would cause an error, since you can’t have the same variable name twice.
The first variable is of type int, which stands for integer. An integer is a whole number, meaning it can’t store decimal points. The type float, which is short for floating-point number, can however store numbers with decimal points. Since we would like our user to input values with decimal points, we will use the type float.
We now know what type of number we would like to use, but how do we convert the string to a float? Luckily this is very simple, we can just use the .to_float() method on our “input” variable. Since C3 isn’t object oriented, you may wonder why it has methods. In C3 methods are just alternative syntax for struct functions. They behave the same as regular functions would but frequently make it easier to read code.
If we implement this in our program it will look like this:
float? input_as_number = input.to_float();The “input_as_number” variable is an optional again, because the .to_float() method can return a fault in some cases (if the input string is only letters for example).
As we did before with the string optional we will again use an if-catch block:
if(catch excuse = input_as_number)
{
io::printn(excuse);
return 1;
}You can use the variable name “excuse” again, because it only exists inside of the if-catch’s scope. This will also be explained in a later chapter. For now just keep in mind that you can reuse variable names if they are inside of the parentheses or curly brackets of an if-statement.
Formatted Printing
Finally we can now convert our input, which is in Celsius, to Fahrenheit. To let the user know that the value he will receive in the end is actually Fahrenheit and not just some random number, it would be nice to add a “°F” at the end of the output.
In order to mix variable output with non-variable output we use the printfn macro, notice how this one has an f before the n. The f stands for formatted, meaning we are printing formatted output. Formatted printing also allows us the use placeholders, in order to supply variables into the output. This is how it’s done:
io::printfn("Formatted variable: %d", variable);If the variable is 10.0 for example the output will be: “Formatted variable: 10”. The %f is the placeholder for our input variable. Placeholders are denoted with a % followed by the type of the variable to be inserted. Since our variable is a float, we will use f. There are also other placeholder types, like: %d (decimal), %x (hexadecimal), %e (scientific notation), %s (string). The last one, %s, is special because you can use it for any type. However %f has a nice feature which we will see later.
In our project we will add the following line, just above return 0:
// The formula to convert Celsius to Fahrenheit is: °F = (°C × 1.8) + 32
io::printfn("Converted to Fahrenheit: %.2f°F", input_as_number * 1.8 + 32);Note
The // syntax starts a comment, this means the compiler will ignore everything after. Comments are used for explaining code.
The placeholder here is the %f but modified. The .2 between the percent symbol and the f tells the printfn macro to only print the first two decimal points of our float. Then as a second argument we pass in our “input_as_number” perform some calculations on it to convert the value from Celsius to Fahrenheit.
Lastly you may want the user to know that they should now enter a number in degrees Celsius. In order to do that you can simply add a io::print() statement before the io::treadline() one. Notice, it doesn’t have a n at the end. The n would create a newline, but since we want to have this text on the same line as our users input we simply use the io::print macro.
Final Code
This is what your code should look like now:
module temperature_converter;
import std::io;
fn int main(String[] args)
{
io::print("Enter degrees in Celsius: ");
String? input = io::treadline();
if(catch excuse = input) {
io::printn(excuse);
return 1;
}
float? input_as_number = input.to_float();
if(catch excuse = input_as_number) {
io::printn(excuse);
return 1;
}
io::printfn("Converted to Fahrenheit: %.2f°F", input_as_number * 1.8 + 32);
return 0;
}Your terminal input and output should look something like this:
Program linked to executable 'build/temperature_converter'.
Launching ./build/temperature_converter
Enter degrees in Celsius: 5.2
Converted to Fahrenheit: 41.36°F
Program completed with exit code 0.Summary
You have learned a lot in this chapter and we will expand on the concepts covered here in later chapters. Don’t worry if you didn’t understand everything yet, the next chapters are solely dedicated to explaining everything that we did in this chapter in greater detail. Be proud of yourself, that you have made it this far! Below you can find additional exercises for practicing.
Practice
Tip
The amount of stars indicates the difficulty of the task. Harder tasks have more stars.
Task 1
Difficulty:
Make the program only print the first decimal point of the Fahrenheit value.
Solution
%.2f placeholder to be %.1f.
Task 2
Difficulty:
Make the program print a more descriptive error message if the “input” variable can’t be converted to a float. Right now it would print: string::MALFORMED_FLOAT which could confuse a user, since they may not know what a float is.
Solution
To do this you can simply replace the io::printn(excuse); line inside of the second if-catch block with a more descriptive message, like so:
io::printn("Input wasn't a number!");