forked from xolatile/xhartae
180 lines
11 KiB
C
180 lines
11 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
|
|
// 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"
|
|
|
|
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;
|
|
}
|
|
}
|
|
@
|
|
|
|
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, and 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". I'm using the simplest solution here, we have one general function, and
|
|
explicitly declaring where I want my data to be printed. Simple as that. Lets call it 'printing_crossroad' for fun.
|
|
*/
|
|
|
|
static void printing_crossroad (char * text, int size, int file, char * string) { // We pass a lot of arguments here, it makes things simpler below.
|
|
if (file > 0) { // We don't want to print to standard input, which is file descriptor of value 0.
|
|
file_write (file, text, size); // And we simply "output" our text of certain size to wanted location.
|
|
} else if (string != NULL) {
|
|
string_copy_limit (string, text, size);
|
|
} else {
|
|
out (text, size);
|
|
}
|
|
}
|
|
|
|
static void print_colour (char colour_id, int file, char * string) {
|
|
switch (colour_id) { // We use "special" character '/' to use terminal colours.
|
|
case '/': printing_crossroad ("/", 1, file, string); break; // If we have literally typed "//" in our 'format' string, it'll just output "/".
|
|
case '0': printing_crossroad ("\033[1;30m", 7, file, string); break; // Notice that we couldn't use function 'terminal_colour', it only uses standard output.
|
|
case '1': printing_crossroad ("\033[1;31m", 7, file, string); break; // Since we want to support file descriptors and strings, we use 'printing_crossroad'.
|
|
case '2': printing_crossroad ("\033[1;32m", 7, file, string); break;
|
|
case '3': printing_crossroad ("\033[1;33m", 7, file, string); break;
|
|
case '4': printing_crossroad ("\033[1;34m", 7, file, string); break;
|
|
case '5': printing_crossroad ("\033[1;35m", 7, file, string); break;
|
|
case '6': printing_crossroad ("\033[1;36m", 7, file, string); break;
|
|
case '7': printing_crossroad ("\033[1;37m", 7, file, string); break;
|
|
case '-': printing_crossroad ("\033[0m", 4, file, string); break;
|
|
default: printing_crossroad ("?", 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 argument_list, int file, char * string) {
|
|
switch (format_id) { // We use character '%' this time, same as 'printf' function does, lets see...
|
|
case '%': {
|
|
printing_crossroad ("%", 1, file, string);
|
|
} break;
|
|
case 'i': {
|
|
int integer; // Leave these local variables uninitialized (don't assign a value to them).
|
|
char * format;
|
|
int length;
|
|
integer = va_arg (argument_list, int); // Macro 'va_arg' will move argument from the list into our local variable, with the provided type.
|
|
length = string_length (format = number_to_string (integer));
|
|
printing_crossroad (format, length, file, string); // You might get the feeling that this isn't type safe, and you'd be totally right.
|
|
} break;
|
|
case 'f': {
|
|
double ieee754; // Because we used curly braces, we can declare local variables in these blocks of code.
|
|
ieee754 = va_arg (argument_list, double); // I intentionally call this IEEE754 because I hate to use 'float' and 'double'.
|
|
char * format;
|
|
// And we're printing to terminal our (rounded) number.
|
|
format = number_to_string ((int) ieee754);
|
|
printing_crossroad (format, string_length (format), file, string);
|
|
} break;
|
|
case 's': {
|
|
char * literal; // Really simple stuff, but needs some time getting used to it, we're writing in an old language.
|
|
literal = va_arg (argument_list, char *); // In my opinion, this should be the part of the C language itself, not some macro black magic.
|
|
// You can write a bunch of variadic functions yourself if you want, to easy your workflow in some cases.
|
|
printing_crossroad (literal, string_length (literal), file, string);
|
|
} break;
|
|
default: {
|
|
printing_crossroad ("?", 1, file, string); // Again, I think it's best to print a bad thing, see it and fix it, then to 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 '%': { out ("%", 1); } break;
|
|
// case 'i': { int integer; integer = va_arg (argument_list, int); echo (number_to_string (integer)); } break;
|
|
// case 'f': { double ieee754; ieee754 = va_arg (argument_list, double); echo (number_to_string ((int) ieee754)); } break;
|
|
// case 's': { char * string; string = va_arg (argument_list, char *); echo (string); } break;
|
|
// default: { out ("?", 1); } 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.
|
|
@
|
|
*/
|
|
|
|
void print (char * format, ...) {
|
|
va_list argument_list; // This is mandatory local variable if we'll use variadic arguments in this function.
|
|
|
|
int offset, length;
|
|
|
|
length = string_length (format);
|
|
|
|
va_start (argument_list, format); // Every variadic function needs to start with this (before using 'va_arg').
|
|
|
|
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], 0, NULL); // 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], argument_list, 0, NULL); // We're calling function that will format our agruments, so we pass variable 'argument_list'.
|
|
} else {
|
|
out (& format [offset], 1); // Not a special character? Okay, we'll just print them one by one.
|
|
}
|
|
}
|
|
|
|
va_end (argument_list); // And every variadic function needs to end with this. Puns intended.
|
|
}
|
|
|
|
void file_print (int file, char * format, ...) {
|
|
(void) file;
|
|
(void) format;
|
|
return;
|
|
}
|
|
|
|
void string_print (char * string, char * format, ...) {
|
|
(void) string;
|
|
(void) format;
|
|
return;
|
|
}
|
|
|
|
#endif
|