Laboratory: Simple String Manipulation

Gábor Horváth / Zsolt Kohári · 2020.10.29.

Low level string handling.

We will need pointers to solve the problems of this lab. Either because of indirect parameters, or because of arrays.

When passing an array as a function parameter, its memory address is passed just like if &t[0] was passed. For this reason usually the size of the array must be passed, too. Except for the strings, where the content determines the end of the array (terminating zero), so we almost never pass the size of a string. Make thoughtful decision at each problem whether to pass the size or not!

Preparation for lab:

  • Review lecture topics on pointers and strings.

1. Vertically

Write a program that asks the user to enter a word, stores it in a string, and prints it letter by letter vertically. For example if you give „Word”, the result should be:

W
o
r
d

You can assume that the word is never longer than 99 characters.

Hint

The easiest way to read a word from the keyboard is to use the scanf function. It will read the letters till the first white space and put them to the array passed to it, including the terminating zero. Make sure that the array passed to scanf is long enough to accomodate the longest expected word.

Solution

#include <stdio.h>

int main(void) {
    char name[100];

    printf("Your first name:\n");
    scanf("%s", name);

    for (int i = 0; name[i] != '\0'; ++i)
        printf("%c\n", name[i]);

    return 0;
}

2. Trimmer

It is a common task to remove leading and trailing spaces from a string. This function is often called trim().

Write a function that removes the spaces from the beginning and from the end of a string (other spaces must stay)! For example if the original string is "  Hi, what's up?   ", then the new string should be "Hi, what's up?". The function should take two parameters: a source array (containing the original string) and a destination array (to put the trimmed string into). You can assume that the destination array is long enough for the resulting string.

Do we need to pass the size of the array? Why?

Hint

1) First find (and remember) the first non-space character from the beginning of the string, 2) then find the end of the string, 3) and starting from there find the first non-space character backwards (the last one in the string). These two positions identify the segment of the string to copy. After copying into destination, do not forget to terminate the destination string.

Write a short program, too, that calls this function and demonstrates that it operates correctly!

Modify your function to allocate space for the trimmed string! What is the difference in calling? Why the resulted string still "alive" outside the function? (Do not forget to erase the memory allocation!)

Solution

#include <stdio.h>

void trim(char *source, char *dest) {
    int begin = 0, end = 0;

    /* Jump spaces at the beginning */
    while (source[begin] != '\0' && source[begin] == ' ')
        begin++;
    /* Where is the end od the string? Then go back and cut the trailing spaces */
    while (source[end] != '\0')
        end++;
    end--;
    while (end >= 0 && source[end] == ' ')
        end--;
    /* Copy to the beginning and close */
    int i;
    for (i = 0; i <= end-begin; i++)
        dest[i] = source[begin+i];
    dest[i] = '\0';
}
int main(void) {
    char s1[100] = "      Hey what's up?   ";
    char s2[100];

    trim(s1, s2);
    printf("[%s] [%s]\n", s1, s2);

    return 0;
}

3. Values backwards

Write a program, that first asks the user the number of real values to read; then it reads the values into a dynamically allocated array. At the end the program should print the elements of the array backwards, and release the allocated memory!

Hint

To store arbitrary many values – not limited by neither the size of a static array nor the size of the stack –, dynamic memory handling is needed. Remember the lecture: malloc() allocates dynamic memory returning the starting address of the block. It can be casted to any type, depending on the type of elements we would like to store in the array. When we do not need the array anymore calling the free() function will release the allocated dynamic memory. Allocating the array must obviously be preceeded by reading the number of values.

Solution

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int cnt;
    printf("How many numbers?\n");
    scanf("%d", &cnt);

    double *numbers = (double*) malloc(sizeof(double) * cnt); // !
    if (numbers == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    printf("Enter %d numbers:\n", cnt);
    for (int i = 0; i < cnt; ++i) {
        scanf("%lf", &numbers[i]);                           // !
    }

    for (int i = cnt-1; i >= 0; --i) {
        printf("%g\n", numbers[i]);
    }

    free(numbers);                                           // !

    return 0;
}

4. Convert to upper case

Write a program that converts all lower case latin letters in a text file to uppercase. Write the result to an other file called "output.txt". Display an error message in case opening the files was not successful.

To test the program, download this file, copy it to the project folder, and execute the program. Then, open "output.txt" with notepad, and check whether its content is correct.

Hint

Detecting and handling the end of file properly is often a critical issue in a file processing program. There are two options to choose from:

  1. Based on the return value of fscanf: Remember that fscanf returns the number of data successfully obtained from the file - if it is less than expected, than probably the end of file has been reached.
  2. Based on feof: The feof function can also be used to detect the end of a file. But there is a catch. feof returns false even after reading the last data from the file. It becomes true only after the next read attempt, that failed due to the end of file. Thus, its value has to be checked after every read attempt and the data processing needs to be stopped when it returns false.

Solution

This solution uses the return value of fscanf to detect the end of file:

#include <stdio.h>
#include <ctype.h>

int main() {
    FILE *fin, *fout;
    char c;

    fin = fopen("lorem.txt", "r");
    if (fin == NULL) {
        printf("Cannot open input file\n");
        return 1;
    }

    fout = fopen("output.txt", "w");
    if (fout == NULL) {
        printf("Cannot open output file\n");
        return 2;
    }

    while (fscanf(fin, "%c", &c) == 1) {
        if (islower(c))
            c = toupper(c);
        fprintf(fout, "%c", c);
    }

    fclose(fin);
    fclose(fout);

    return 0;
}

5. Line wrap

Write a program that opens a text file and breaks all lines wider than 20 charaters. The output should be written to "output.txt".

To test the program, download this file, copy it to the project folder, and execute the program. Then, open "output.txt" with notepad, and check whether its content is correct.

Solution

#include <stdio.h>

int main() {
    FILE *fin, *fout;
    char c;
    int line_len = 0;

    fin = fopen("lorem.txt", "r");
    if (fin == NULL) {
        printf("Cannot open input file\n");
        return 1;
    }

    fout = fopen("output.txt", "w");
    if (fout == NULL) {
        printf("Cannot open output file\n");
        return 2;
    }

    while (fscanf(fin, "%c", &c) == 1) {
        if (c == '\n') {
            line_len = 0;
        }
        else if (line_len == 20) {
            fprintf(fout, "\n");
            line_len = 0;
        }
        fprintf(fout, "%c", c);
        line_len++;
    }

    fclose(fin);
    fclose(fout);

    return 0;
}

6. Job assignments in a file

Write a C program tho manage job assignments. Each job assignment has

  • a unique identifier (an integer number),
  • the name of the employee who has to do the job (max. length: 100 characters),
  • the description of the job (max. length: 200 characters),
  • and a status, which is a 1/0 value indicating that the job is done or not done yet.

The file containing job assignments starts with an integer number, representing the total number of jobs in the file, then the members of the structure follow each other in separate lines.

The C code below creates a structure to represent job assignments, and loads job assignments from file "jobs.txt" into a dynamic array.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int id;
    char name[101];
    char descr[201];
    int status;
} job;

int main () {
    FILE* infile;   // file handle
    job* a;         // start address of the dynamic array    
    int N;          // number of elements in the dynamic array
    int i;          // auxiliary variable for loops
    
    // open file
    infile = fopen("jobs.txt","r");
    if (infile==NULL) {
        printf("Cannot open jobs.txt\n");
        return 1;
    }
    
    // load the content of the file into a dynamic array
    fscanf(infile, "%d", &N);   // first integer is the number of items
    a = (job*)malloc(sizeof(job)*N);
    for (i = 0; i < N; i++) {
        fscanf(infile, "%d\n", &a[i].id);
        fgets(a[i].name, 101, infile);       // we use fgets to allow space characters in the name
        a[i].name[strlen(a[i].name)-1] = '\0'; // last newline character is deleted
        fgets(a[i].descr, 201, infile);        
        a[i].descr[strlen(a[i].descr)-1] = '\0';
        fscanf(infile, "%d\n", &a[i].status);
    }
    fclose(infile);
 
    free(a);
    return 0;
}

Note that we used fgets to read the name and the description, to allow spaces. Since fgets keeps the newline character and the end of the line, we had to remove it manually.

The tasks are as follows.

  • Write a function to print all jobs assigned to "Joe"
  • Set the status of all jobs assigned to "Joe" to 1.
  • Save the modified data to "jobs2.txt".

To test the program, download this file, copy it to the project folder, and execute the program. Then, open "jobs2.txt" with notepad, and check whether its content is correct.

Solution

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int id;
    char name[101];
    char descr[201];
    int status;
} job;

int main () {
    FILE *infile, *outfile;   // file handle
    job* a;         // start address of the dynamic array    
    int N;          // number of elements in the dynamic array
    int i;          // auxiliary variable for loops
    
    // open file
    infile = fopen("jobs.txt","r");
    if (infile==NULL) {
        printf("Cannot open jobs.txt\n");
        return 1;
    }
    
    // load the content of the file into a dynamic array
    fscanf(infile, "%d", &N);   // first integer is the number of items
    a = (job*)malloc(sizeof(job)*N);
    for (i = 0; i < N; i++) {
        fscanf(infile, "%d\n", &a[i].id);
        fgets(a[i].name, 101, infile);       // we use fgets to allow space characters in the name
        a[i].name[strlen(a[i].name)-1] = '\0'; // last newline character is deleted
        fgets(a[i].descr, 201, infile);        
        a[i].descr[strlen(a[i].descr)-1] = '\0';
        fscanf(infile, "%d\n", &a[i].status);
    }
    fclose(infile);
 
    // printing jobs
    for (i = 0; i < N; i++) {
        if (strcmp(a[i].name, "Joe") == 0) {
            printf("\nJob #%d\n", a[i].id);
            printf("Description: %s\n", a[i].descr);
            printf("Status: %d\n", a[i].status);
        }
    }

    // setting the status
    for (i = 0; i < N; i++) {
        if (strcmp(a[i].name, "Joe") == 0)
            a[i].status = 1;
    }

    // saving it to file
    outfile = fopen("jobs2.txt", "w");
    if (outfile==NULL) {
        printf("Cannot open jobs2.txt\n");
        return 2;
    }    

    // save number of jobs first
    fprintf(outfile, "%d\n", N);
    // save all job assignments
    for (i = 0; i < N; i++)
        fprintf(outfile, "%d\n%s\n%s\n%d\n", a[i].id, a[i].name, a[i].descr, a[i].status);

    fclose(outfile);
    
    free(a);
    return 0;
}