# Created 2024-07-07 dim. 16:01
#+title: 7d.nz
#+author: Phil. Estival
* • [2019-03-04 lun.] Calculating the students marks                   :code:
in C, Python, Go and Awk

>> French translation

./img/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:
#+begin_src text
  | BAACH Leila          |  6 |  9 |  9 |
  | BARREIRO Bruno       |  9 |  0 | 10 |
  | COURAULT Eva         |  2 |  8 |  0 |
  | DELL OVA Laura       |  3 |  0 |  3 |
#+end_src
** Generating the data file
We generate the dataset with the following script

#+name: TestSet
#+begin_src python :results output replace
  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")
#+end_src


#+name: marks
#+begin_src sh :results raw replace
  cat /tmp/students.txt
#+end_src
** Plain ol'C
*** Pre required C functions
**** delete unneeded spaces in strings before integer cast
example : string "  42 " is converted to integer 42

#+name: trimwhitespace
#+begin_src C
  /*
    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;
  }
#+end_src
**** 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.

#+name: fields
#+begin_src C :results output replace :noweb yes
  /**
     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;
  }
#+end_src
*** C Ansi 99

The resulting program using the previous
functions (the program in weaved and tangled once
after an export to html).
#+name: C-marks
#+begin_src C :results replace raw output :noweb yes
  /**
  Scinder une chaîne en champs
  ,**/
   #include <stdlib.h>
   #include <string.h>

   const char* champ(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;
   }

  /*
    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);
   }
#+end_src
** Python
#+name: PythonMarks
#+begin_src python :results replace raw output :tangle /tmp/marks.py
  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)
#+end_src
** Go

#+begin_src go :results raw output replace
  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)
      }
  }
#+end_src
** Awk

add awk to ob-babel,
and run without asking for confirmation when pressing C-c C-c
#+begin_src elisp
  (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)))
#+end_src
*** awk-sh

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

#+begin_src sh :var (criteria . 180) :results raw replace
   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
#+end_src


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

#+name: awk-notes
#+begin_src awk :var (critere . 180) :var (IFS . |) :var (FS .  ) :results raw replace :stdin marks
  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
  }
#+end_src
*** Ob-awk

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

Declare the header
#+name: header
#+begin_src awk
  printf "|Student|French|Maths|Hist.Geo|Total|Average\n|-\n";
#+end_src

Declare field separator
#+name: separator
#+begin_src awk
  FS="\t";OFS="|"
#+end_src
The display function for one line
#+name: print
#+begin_src awk
  printf "|%s|%d|%d|%d|%d|%.1f\n", $1,$2,$3,$4, total, total/24
#+end_src

Total computation
#+name: total
#+begin_src awk
  total = 10*$2 + 8*$3 + 6*$4;
#+end_src

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

#+name: upper
#+begin_src awk :var (limit . 170) :results raw replace :noweb yes :stdin marks
  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
  }
#+end_src

Here's an other variant with an interval
#+begin_src awk :results raw replace :noweb yes :stdin marks
  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
  }
#+end_src

C-c ^ SPC to sort

#+results:
| Student | French | Maths | Hist.Geo | Total | Average |
|---------+--------+-------+----------+-------+---------|
** Checking the results
Finally, resulting arrays should be checked to
ensure the results are correct.