7d.nz

Calculating the students marks

[2019-03-04 lun.]

  code

in C, Python, Go and Awk

>> French translation

monk.png

We are comparing 4 programming languages for the following problem :

On your marks

By the end of the year, a teacher wishes to evaluate the results of his pupils by weighting the marks obtained in three disciplines: french (weight 10), maths (weight 8), history & geography (weight 6). Marked over 10, the maximum sum of the weighted marks is 240.

As a result, the pupils are classified in three groups: those with at least 180, those with at least 120 and the others, who have less that 120. Is printed for each group, the name of the pupil, the marks obtained in each discipline and the average weighted mark (weighted sum divided by 24)

And here's what we can tell when comparing the 4 code variants:

  • The C program is the longest as we'll have to manage data input with low-level functions.
  • the easiest is Python, because Python is easy, readable and fast to program
  • and the winner is ob-awk: most elegant, shortest and suitable solution for that kind of program.

Data

The data are in text-file, CSV-like, organized in a table like so:

  | BAACH Leila          |  6 |  9 |  9 |
| BARREIRO Bruno       |  9 |  0 | 10 |
| COURAULT Eva         |  2 |  8 |  0 |
| DELL OVA Laura       |  3 |  0 |  3 |

Generating the data file

We generate the dataset with the following script

  from random import randint
elv = """\
BAACH Leila
BARREIRO Bruno
COURAULT Eva
DELL OVA Laura
DUGUET Pierre
ESCANDE Thomas
GATTO Marianne
GUIRAUD Sarah
LHEUILLIER Margaux
LONGO Tania
MARTIN Caroline
MARTY Lea
MATTIA Louis
MORIN Jules
PASCALE Eva
PAYSSERAND Laura
RIBEIRO SOCORRO Jade
ROMERO Bastien
ROTY Elodie
RUTIGLIANO Marine
SAFOURCADE Morgane
ZOUINI Nada"""

with open("/tmp/students.txt","w") as f:
    for e in elv.split('\n'):
        el = ("| %s | %i | %i | %i |" % (
            e, *[randint(0,10) for x in range(3)]))
        print(el)
        f.write(el+"\n")
  cat /tmp/students.txt

Plain ol'C

Pre required C functions

delete unneeded spaces in strings before integer cast

example : string " 42 " is converted to integer 42

  /*
  delete unneeded spaces in strings before integer cast
  example : string "  42 " is converted to integer 42
*/
#include <string.h>

// characters considerer as space (there could be more than that)
#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')

char *trimwhitespace(const char *str)
{
  char *end;

  // trim prefix
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)
    return str;

  // trim siuffix
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;

  // add null terminator
  end[1] = '\0';

  return str;
}

Split string and retrieve fields

It's non optimal for the problem at hand, but makes code more readable. A correct solution would run the tokenizer once per line.

  /**
   Split string into fields
**/
#include <stdlib.h>
#include <string.h>

const char* field(char* line, const char * c, int num)
{
  char * tmp = strdup(line);
  const char* tok;
  for (tok = strtok(tmp, c);
       tok && *tok;
       tok = strtok(NULL, c))
    {
      if (!--num) {
        return tok;
      }
    }
  return NULL;
}

C Ansi 99

The resulting program using the previous functions (the program in weaved and tangled once after an export to html).

  
/*
  delete unneeded spaces in strings before integer cast
  example : string "  42 " is converted to integer 42
*/
#include <string.h>

// characters considerer as space (there could be more than that)
#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')

char *trimwhitespace(const char *str)
{
  char *end;

  // trim prefix
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)
    return str;

  // trim siuffix
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;

  // add null terminator
  end[1] = '\0';

  return str;
}
#include <stdlib.h>
#include <stdio.h>

// Coefficients pour chaque matière
const struct Coeff {
   int francais;
   int maths;
   int histgeo;
} coeff = {
   .francais = 10,
   .maths = 8,
   .histgeo = 6,
};

// Total des coefficients
int total_coeff;

typedef struct Eleve {
   char * nom;
   int francais, maths, histgeo, total;
   float moy;
} Eleve;


Eleve ** tab180;
Eleve ** tab120;
Eleve ** tabinf;

// l'unité de reservation mémoire pour les tableaux dynamiques
const VSZ = 16;

// affichage des tableaux d'élèves
void affiche_liste(Eleve ** eleves, int nbe) {
   printf("|%-22s|%8s|%5s|%7s|%3s|%5s|\n",
            "Nom","français","maths","histgeo","moy","total");
   printf("|-\n");
   for (int i=0; i<nbe; i++){
     printf("|%-22s|%8d|%5d|%7d|%.1f|%5d|\n",
         eleves[i]->nom,
   eleves[i]->francais,
   eleves[i]->maths,
   eleves[i]->histgeo,
   eleves[i]->moy,
   eleves[i]->total);
   }
}

// programme principal
int main()
{
    FILE *fp; // le descripteur du fichier
    fp = fopen("/tmp/eleves.txt" , "r");

    char line[128]; // tampon pour la lecture des lignes du fichier

    // allocation des tableaux
    tab180 = malloc(VSZ*sizeof(void*));
    tab120 = malloc(VSZ*sizeof(void*));
    tabinf = malloc(VSZ*sizeof(void*));
    int v180 = 1, v120 = 1, vinf = 1;
    int n180=0, n120=0,ninf=0;

    total_coeff = coeff.francais + coeff.maths + coeff.histgeo;

    while (fgets(line, 128, fp)) // lecture d'une ligne du fichier
    {
      Eleve * eleve = malloc(sizeof(Eleve));
      eleve->nom = trimwhitespace(champ(line, "|", 1));
      eleve->francais = atoi(trimwhitespace(champ(line, "|", 2)));
      eleve->maths = atoi(trimwhitespace(champ(line, "|", 3))),
      eleve->histgeo = atoi(trimwhitespace(champ(line, "|", 4))),
      eleve->total =
           eleve->francais * coeff.francais
         + eleve->maths * coeff.maths
         + eleve->histgeo * coeff.histgeo;

      eleve->moy =(float) eleve->total / (float)total_coeff;

      if(eleve->total >= 180) {
         tab180[n180] = eleve;
   n180++;
      }else if(eleve->total >= 120) {
   tab120[n120] = eleve;
         n120++;
      }else {
   tabinf[ninf] = eleve;
         ninf++;
      }

      if(n180 > VSZ*v180) {
         realloc(tab180, v180 * VSZ * sizeof(void*));
   v180++;
      }
      if(n120 > VSZ*v120) {
         realloc(tab120, v120 * VSZ * sizeof(void*));
   v120++;
      }
      if(ninf > VSZ*vinf) {
         realloc(tabinf, vinf * VSZ * sizeof(void*));
   vinf++;
      }
    }

    printf("\ntab180\n");
    affiche_liste(tab180,n180);
    printf("\ntab120\n");
    affiche_liste(tab120,n120);
    printf("\ntabinf\n");
    affiche_liste(tabinf,ninf);
    fclose(fp);
 }

Python

  coeff = {
     "français":10,
     "maths":8,
     "histgeo":6
}
# total coefficients is 24
total_coeffs = sum(coeff.values())

# initialize 3 arrays
tab180=[]
tab120=[]
tabinf=[]

# open and read the arrays
with open("/tmp/students.txt") as f:
     L = f.readline()
     while L:

        fields  = {"nom":""}
        # line is split according the array delimiter
        nom,francais,maths,histgeo = L.split('|')[1:-1]
        #nom,francais,maths,histgeo = L.split('|')[1:-1]
        # marks are cast to int
        francais,maths,histgeo = int(francais), int(maths), int(histgeo)
        eleve = {
             "nom":nom,
             "français": francais,
             "maths": maths,
             "histgeo": histgeo,
        }

        total = 0
        # total score
        for k in coeff.keys():
             total  += coeff[k] * eleve[k]
             # compute means
             moy = float(total) / float(total_coeffs)

        eleve.update( {
             "moy": moy,
             "total": total,
        })

        # sort according the scores
        if(total>=180):
             tab180.append(eleve)
        elif(total >= 120):
             tab120.append(eleve)
        else:
             tabinf.append(eleve)
             # read next line
             L = f.readline()


# display
def display_list(li):
     print(
          "|{0:22s}|{1:9s}|{2:5s}|{3:7s}|{4:3s}|{5:5s}|".format(
               "Nom","français","maths","histgeo","moy","total"
          ))
     print("|-")
     for eleve in li:
          print(
               "|{0:22s}|{1:8d}|{2:5d}|{3:7d}|{4:.1f}|{5:5d}|".format(
                    eleve["nom"],
                    eleve["français"],
                    eleve["maths"],
                    eleve["histgeo"],
                    eleve["moy"],
                    eleve["total"],
               ))
          # affichage des trois listes
print("\ntab180")
display_list(tab180)
print("\ntab120")
display_list(tab120)
print("\ntabinf")
display_list(tabinf)

Go

  import (
   "fmt"
   "strings"
   "io/ioutil"
   "strconv"
)

// split a string according to a delimiter in a array
func split(s string, r rune) []string {
    return strings.FieldsFunc(s, func(c rune) bool { return c == r })
}

type Student struct
{
  name string
  marks map[string]int
  total int
  mean float32
}

var Coeff map[string]int

var tab180, tab120, tabinf []Student

func trim_int(s string) int {
   i, err := strconv.Atoi(strings.TrimSpace(s))
   if(err!=nil) { panic(err) }
   return i
}

func main() {

   Coeff := make(map[string]int)
   Coeff["french"] = 10
   Coeff["maths"] = 8
   Coeff["histgeo"] = 6

   var CoeffTotal = 0
   for k := range Coeff {
CoeffTotal +=  Coeff[k]
   }

   list, _ := ioutil.ReadFile("/tmp/students.txt")
   line := split(string(list), '\n')
   for i := range(line) {
 fields := split(line[i], '|')
 stud := Student{
    name: strings.TrimSpace(fields[0]),
    marks: map[string]int{
  "french": trim_int(fields[1]),
  "maths": trim_int(fields[2]),
  "histgeo": trim_int(fields[3])},
    mean:0,
    total:0 }

 for k := range stud.marks {
    stud.total += Coeff[k] * stud.marks[k]
 }
 stud.mean = float32(stud.total) / float32(CoeffTotal)

 if (stud.total >= 180) {
    tab180 = append(tab180, stud)
 }else if(stud.total >= 120) {
    tab120 = append(tab120, stud)
 }else{
    tabinf = append(tabinf, stud)
 }
    }

    fmt.Println("tab180")
    Affiche_Tableau(tab180)
    fmt.Println("tab120")
    Affiche_Tableau(tab120)
    fmt.Println("tabinf")
    Affiche_Tableau(tabinf)
}


func Affiche_Tableau(eleves []Student) {
   fmt.Printf( "|%-22s|%8s|%5s|%7s|%3s|%5s|\n",
  "Name","french","maths","histgeo","moy","total" )
   for i := range eleves {
 fmt.Printf("|%-22s|%8d|%5d|%7d|%.1f|%5d|\n",
   eleves[i].name,
   eleves[i].marks["français"],
   eleves[i].marks["maths"],
   eleves[i].marks["histgeo"],
   eleves[i].mean,
   eleves[i].total)
    }
}

Awk

add awk to ob-babel, and run without asking for confirmation when pressing C-c C-c

  (defun my-org-confirm-babel-evaluate (lang body)
  (not  (or
  (string= lang "awk")
  (string= lang "sh"))))
(setq org-confirm-babel-evaluate 'my-org-confirm-babel-evaluate)
   (org-babel-do-load-languages
     'org-babel-load-languages
     '((awk . t)))

awk-sh

Here the variable $criteria will make a problematic travel as org > sh > awk. 180 ends hard coded.

   awk '
BEGIN {printf "|Name|French|Maths|Hist.geo|total|moyenne\n|-"}
 { total= 10*$3 + 8*$4 + 6*$5;
   if (total > 180)
     printf "|%s|%d|%d|%d|%d|%d\n", $2, $3, $4, $5, total, total/24}'\
 FS="|" OFS="|" /tmp/students.txt

Here is it cleaner, with IFS and OFS variable as preamble

  BEGIN {printf "|Nom|French|Maths|Hist.geo|total|moyenne\n|-"}
{
    total= 10*$2 + 8*$3 + 6*$4;
    if (total > critere)
        printf "|%s|%d|%d|%d|%d|%d\n", $1, $2, $3, $4, $5, total, total/24
}

Ob-awk

With ob-awk and noweb, here's how to assemble the program in a proper way.

Declare the header

  printf "|Student|French|Maths|Hist.Geo|Total|Average\n|-\n";

Declare field separator

  FS="\t";OFS="|"

The display function for one line

  printf "|%s|%d|%d|%d|%d|%.1f\n", $1,$2,$3,$4, total, total/24

Total computation

  total = 10*$2 + 8*$3 + 6*$4;

And the final program, modularized with noweb and with a default variable. It can also be called like so #+Call: upper(120)

  BEGIN { printf "|Student|French|Maths|Hist.Geo|Total|Average\n|-\n"; FS="\t";OFS="|" }
{ total = 10*$2 + 8*$3 + 6*$4;
  if (total >= limit) printf "|%s|%d|%d|%d|%d|%.1f\n", $1,$2,$3,$4, total, total/24
}

Here's an other variant with an interval

  BEGIN { printf "|Student|French|Maths|Hist.Geo|Total|Average\n|-\n"; FS="\t";OFS="|" }
{ total = 10*$2 + 8*$3 + 6*$4;
  if (total > 120 && total < 180) printf "|%s|%d|%d|%d|%d|%.1f\n", $1,$2,$3,$4, total, total/24
}

C-c ^ SPC to sort

Checking the results

Finally, resulting arrays should be checked to ensure the results are correct.