xhartae/chapter/chapter_3.c

197 lines
14 KiB
C

/*
Copyright (c) 2023 : Ognjen 'xolatile' Milan Robovic
Xhartae is free software! You will redistribute it or modify it under the terms of the GNU General Public License by Free Software Foundation.
And when you do redistribute it or modify it, it will use either version 3 of the License, or (at yours truly opinion) any later version.
It is distributed in the hope that it will be useful or harmful, it really depends... But no warranty what so ever, seriously. See GNU/GPLv3.
*/
#ifndef CHAPTER_3_SOURCE
#define CHAPTER_3_SOURCE
#include "chapter_3.h"
/*
This is probably a good time to show you how switch statement works, and two (in my opinion best) ways to align them, but I advise against mixing two alignments you'll see in
'print_colour' and 'print_format' functions. Also, it's a good practice to always use 'default' keyword at the end of you switch statements. You don't always need to use 'break'
at the end of 'case', but we intend to "end" the switch statement there, so it's fine. Cases can be "fall-through", so if you don't put 'break', it'll execute the code in the next
one, or you could have several of cases before any code. I don't like to use them like that, but here are few examples:
@C
// If argument 'character' is any of the uppercase letters, it returns TRUE, if lowercase, it returns FALSE, otherwise exits the program.
static bool hexadecimal_letter_is_uppercase (char character)
switch (character) {
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': return (TRUE); // We don't need to break here, because these two return from function.
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': return (FALSE); // If there was code after the switch statement, we would use 'break' keyword.
default: exit (EXIT_FAILURE); // We don't need to break here yet again, because we exit a program!
}
}
@
You can use only integers in cases, and remember, 'char' is implicitly promoted to 'int', aka integer. And important thing to keep in mind is that you can write every switch
statement as if-else statement, but it would be longer, more error-prone, some lines of code would repeat, and many more inconveniences would come, especially because in C, cases
can fall-through, like in the example below. Sad truth is, people are dumb, and many compilers and linters will output a warning message complaining that you used fall-through
case(s) in your switch statement. Lets explain what 'echo_one_by_one' function would do.
@C
static void echo_one_by_one (int number) {
switch (number) {
case 0: echo ("Zero ");
case 1: echo ("One ");
case 2: echo ("Two ");
case 3: echo ("Three ");
default: break;
}
}
// Print out some stuff, it'll look like this for these inputs (and it'll do nothing for input that isn't between 0 and 3 (inclusive)):
// echo_one_by_one (0) => "Zero One Two Three"
// echo_one_by_one (1) => "One Two Three"
// echo_one_by_one (2) => "Two Three"
// echo_one_by_one (3) => "Three"
@
You can find situations in which fall-through cases are good, for example, they can be very useful when encoding some CPU instructions into machine code, but guess what? The
compiler will think you've made some kind of mistake, like that you forgot to break from those cases, and it'll warn you about it. I like to clean all compiler warnings (and some
linter warnings, if they're not totally brain-dead), so I just don't use them. I know, sounds stupid, but there's usually some other way to do it, to get the same solution. Since
we have several methods for printing text, they use standard output (terminal), file descriptor and a string respectively, we could implement them in separate functions, use
function pointers or simply copy+paste bunch of code into lot of functions, and form a "function family". Lets do something very simple and straight forward. We'll end up with
repeated code, but sometimes the simplicity can benefit us more than some smart solutions that are harder to understand. I'll explain as we progress...
*/
static void to_output (char * data, int size, int file, char * string) { (void) file; (void) string; out ( data, size); }
static void to_file (char * data, int size, int file, char * string) { (void) string; file_write (file, data, size); }
static void to_string (char * data, int size, int file, char * string) { (void) file; string_concatenate_limit (string, data, size); }
/*
Lets break down what's going on here, since it might be confusing for beginners. We've defined 3 internal functions, that'll only be used in this file and no other. They look
similar, and they ignore some of their arguments by casting them to 'void', that's how you silence the compiler warnings about unused function agruments. But why are we passing
those arguments if we won't use them? Because we can safely use one function pointer to any of those 3 functions. Now, before we proceed, variables can hold memory addresses of
other variables, constants and functions. We use that fact now.
Internal variable 'printing' is a function pointer to one of those 3 functions, and the default value for it is the memory address of function 'to_output'. So, if we just use it,
by default it'll print to standard output. If we call functions below, they'll change the value of 'printing' to coresponding function memory address. I chose to use concatenation
for 'print_string', but if you wanted, you could use something else, or just reinitialize it to null string, and think about memory before using it. Unlike 'printf' function from
<stdio.h> header file, my print functions don't allocate memory or make buffers.
*/
static void (* printing) (char * data, int size, int file, char * string) = to_output;
static void print_colour (char colour_id, int file, char * string) {
switch (colour_id) { // We use "special" character '/' to use terminal colours.
case '/': (* printing) ("/", 1, file, string); break; // If we have literally typed "//" in our 'format' string, it'll just output "/".
case '0': (* printing) ("\033[1;30m", 7, file, string); break; // Notice that we couldn't use function 'terminal_colour', it only uses standard output.
case '1': (* printing) ("\033[1;31m", 7, file, string); break; // Since we want to support file descriptors and strings, we use 'printing'.
case '2': (* printing) ("\033[1;32m", 7, file, string); break; // Also, we're dereferencing 'printing' pointer, which essentially calls one of the 3 functions.
case '3': (* printing) ("\033[1;33m", 7, file, string); break;
case '4': (* printing) ("\033[1;34m", 7, file, string); break;
case '5': (* printing) ("\033[1;35m", 7, file, string); break;
case '6': (* printing) ("\033[1;36m", 7, file, string); break;
case '7': (* printing) ("\033[1;37m", 7, file, string); break;
case '-': (* printing) ("\033[0m", 4, file, string); break;
default: (* printing) ("?", 1, file, string); break; // Now, if we provided some other character after "/", I like to make an intentional mismatch.
} // It's not such a big mistake to abort the program, since it's obvious that results are bad.
}
static void print_format (char format_id, va_list list, int file, char * string) {
switch (format_id) { // We use character '%' this time, same as 'printf' function does, lets see...
case '%': {
(* printing) ("%", 1, file, string);
} break;
case 'i': {
int integer; // Leave these local variables uninitialized (don't assign a value to them).
char * format; // We'll use format here and below in order to shorten the length of lines.
integer = va_arg (list, int); // Macro 'va_arg' will pop an argument from the list, with the provided type.
format = number_to_string (integer);
(* printing) (format, string_length (format), file, string); // You might get the feeling that this isn't type safe, and you're totally right.
} break;
case 'f': {
double ieee754; // Because we used curly braces, we can declare local variables in these blocks of code.
char * format;
ieee754 = va_arg (list, double); // I intentionally call this IEEE754 because I hate to use 'float' and 'double'.
format = number_to_string ((int) ieee754); // And we're printing to terminal our (rounded) number.
(* printing) (format, string_length (format), file, string);
} break;
case 's': {
char * format; // Really simple stuff, but needs some time getting used to it.
format = va_arg (list, char *); // In my opinion, this should be the part of the C language itself, but oh well...
(* printing) (format, string_length (format), file, string); // This could be written even shorter, but it'd require more mental-overhead.
} break;
default: {
(* printing) ("?", 1, file, string); // Lets not abort the program in this case...
} break;
}
// This entire switch statement can be written more shortly (in terms of lines of code) like this:
// switch (format_id) {
// case '%': { ... } break;
// case 'i': { ... } break;
// case 'f': { ... } break;
// case 's': { ... } break;
// default: { ... } break;
// }
// Choose your own preference with switch statement (and any other one), and stay consistent with how you format it.
// Some people prefer more shorter lines of code, others prefer less longer lines of code (like I do).
}
/*
Before we take a look at the actual implementation of our simple 'print' function, here's how you'd use it.
@C
print ("My integer is %i.\n", 404); // Prints "My integer is 404." with new line.
print ("Heyo %s %s", "world", "#6!\n"); // Prints "Heyo world #6!" with new line.
print ("/1Cyaa world!/-\n"); // Prints red "Cyaa world!" with new line.
@
Now, since we're dealing with unsafe macros, no type-checking, and in general code that I don't like to write, we have that one general printing function, which splits into 3
functions, for standard output, file descriptor and string. Our general function is 'print_select', and it'll handle colouring and formating special characters for us, calling
other functions we defined previously, 'print_colour' and 'print_format'. Since those 2 functions are used only once, we could just place (inline) them where they're called, but
that way we'd end up with very indented code, which I'm not a fan of. I don't follow any indentation rules, I just don't indent a lot.
*/
static void print_select (char * format, va_list list, int file, char * string) {
int offset, length;
length = string_length (format);
for (offset = 0; offset != length; ++offset) { // We start iterating through our 'format' string, and looking for special characters below.
if (format [offset] == '/') { // Colouring special character is '/', and colours are specified from '0'...'7', and '-' to cancel them.
++offset;
print_colour (format [offset], file, string); // We're calling function that will colour our printed text, this one is simple.
} else if (format [offset] == '%') { // And formatting special character is '%', it'll use variadic arguments!
++offset;
print_format (format [offset], list, file, string); // We're calling function that will format our agruments, so we pass variable 'list'.
} else {
(* printing) (& format [offset], 1, file, string); // Not a special character? Okay, we'll just print them one by one.
}
}
}
/*
Now, lets break down what those last 3 very similar functions do. Everything I say about 'print', is same for the other two, but with exception to their output.
@C
void print (char * format, ...) {
va_list list; // Every variadic function needs to have this list, and sometimes you need to pass it to other functions.
printing = to_output; // We're selecting different output method, notice the difference between 3 functions below, it's minimal, and this is part of it.
va_start (list, format); // Every variadic function needs to start with this macro (or function depending on the implementation in <stdarg.h>).
print_select (format, list, 0, NULL); // And we just call our general super smart function to handle everything for us.
va_end (list); // Every variadic function needs to end with this macro... Pun intended.
}
@
Also, needless to say, I don't usually align nor write my programs like this, I did it so you can compare those 3 functions easier. You can see what's changed, what's ignored or
what's same. If you're writing something serious, align it properly, not like this please.
*/
void print ( char * format, ...) { va_list list; printing = to_output; va_start (list, format); print_select (format, list, 0, NULL); va_end (list); }
void file_print (int file, char * format, ...) { va_list list; printing = to_file; va_start (list, format); print_select (format, list, file, NULL); va_end (list); }
void string_print (char * string, char * format, ...) { va_list list; printing = to_string; va_start (list, format); print_select (format, list, 0, string); va_end (list); }
#endif