#+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
#include <string.h>
#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
char *trimwhitespace(const char *str)
{
char *end;
while(isspace((unsigned char)*str)) str++;
if(*str == 0)
return str;
end = str + strlen(str) - 1;
while(end > str && isspace((unsigned char)*end)) end--;
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
#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
#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;
}
#include <string.h>
#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
char *trimwhitespace(const char *str)
{
char *end;
while(isspace((unsigned char)*str)) str++;
if(*str == 0)
return str;
end = str + strlen(str) - 1;
while(end > str && isspace((unsigned char)*end)) end--;
end[1] = '\0';
return str;
}
#include <stdlib.h>
#include <stdio.h>
const struct Coeff {
int francais;
int maths;
int histgeo;
} coeff = {
.francais = 10,
.maths = 8,
.histgeo = 6,
};
int total_coeff;
typedef struct Eleve {
char * nom;
int francais, maths, histgeo, total;
float moy;
} Eleve;
Eleve ** tab180;
Eleve ** tab120;
Eleve ** tabinf;
const VSZ = 16;
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);
}
}
int main()
{
FILE *fp;
fp = fopen("/tmp/eleves.txt" , "r");
char line[128];
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))
{
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_coeffs = sum(coeff.values())
tab180=[]
tab120=[]
tabinf=[]
with open("/tmp/students.txt") as f:
L = f.readline()
while L:
fields = {"nom":""}
nom,francais,maths,histgeo = L.split('|')[1:-1]
francais,maths,histgeo = int(francais), int(maths), int(histgeo)
eleve = {
"nom":nom,
"français": francais,
"maths": maths,
"histgeo": histgeo,
}
total = 0
for k in coeff.keys():
total += coeff[k] * eleve[k]
moy = float(total) / float(total_coeffs)
eleve.update( {
"moy": moy,
"total": total,
})
if(total>=180):
tab180.append(eleve)
elif(total >= 120):
tab120.append(eleve)
else:
tabinf.append(eleve)
L = f.readline()
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"],
))
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"
)
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.