momente şi schiţe de informatică şi matematică
anti point—and—click

Un plugin jQuery pentru calculul dinamic al mediei şcolare

jQuery | media școlară birocratică | plugin
2009 jun

Faceţi click pe un nume din tabelul următor; apoi, introduceţi notele fără spaţiu între ele.

NumeMedia
Ionescu George
Popescu Teofil

Elementelelor HTML care conţin numele le-a fost montat câte un handler de click - funcţia mediasco(), introdusă aici sub forma unui plugin jQuery; aceasta creează trei elemente <input> - pentru note, teză şi "OK" - asociind primelor două câte un handler de eveniment keyup, cu rolul de a calcula şi expune media după fiecare tastare a unei note; click "OK" rotunjeşte media finală şi elimină elementele adăugate.

Media şcolară: 0.5 versus 0.49 (de fapt, 0.4(9))

În sistemul şcolar românesc actual cunoştinţele elevilor sunt evaluate periodic, la fiecare obiect; rezultatele se înscriu în anumite documente oficiale (catalog, registru matricol - păstrate în arhiva şcolii vreme de… 50 de ani). Pe parcursul unui semestru şcolar, la fiecare obiect din catalog se înregistrează pentru fiecare elev cel puţin câte două note; la unele obiecte este prevăzută şi susţinerea unei lucrări (de sinteză chipurile) denumită generic teză.

Notele (inclusiv pentru teză) sunt numere întregi 1..10. La încheierea semestrului, se calculează şi se înscrie în catalog media la fiecare obiect (pentru fiecare elev). Formula de calcul actuală este:

Media = ( (media aritmetică a Notelor) × 3  +  Teza ) / 4  cu două zecimale exacte
Media înscrisă = round(Media)  rotunjeşte la cel mai apropiat întreg

Însă în cazul în care formula este modelată mecanic, impunerea de a calcula "cu două zecimale exacte" (rotunjind numai în final) poate induce incorectitudini; de exemplu, folosind javaScript:

var n1 = 8, n2 = 9, n3 = 9, tz = 8; // trei note şi teza
var Ma = (n1 + n2 + n3) / 3; // media aritmetica a notelor
var temp = Ma * 3 + tz;
var media_c = temp / 4; // media şcolară calculată
var media = Math.round(media_c); // media de înscris în catalog
alert("media aritmetică = " + Ma + "\nori 3 + teza = " + temp +
      "\nsupra 4 = " + media_c + "\nmedia = " + media);

Se alert()-ează ca medie calculată 8.50 (şi rezultă media şcolară 9) - ceea ce este "incorect": media calculată "cu două zecimale exacte" este 8.49, rezultând ca "medie şcolară" 8 (şi nu 9).
Este drept că 8.4(9) = 8.5 dar… numai la matematică (chit că elementară).

În condiţiile reprezentării floating point standard, modelarea corectă a calculelor cu n zecimale exacte se bazează pe un principiu simplu: se lucrează cu întregul care rezultă deplasând virgula spre dreapta cu n poziţii zecimale şi ignorând restul zecimalelor. Astfel, reluând exemplul anterior:

var n1 = 8, n2 = 9, n3 = 9, tz = 8; var Ma = (n1 + n2 + n3) / 3; 
Ma = parseInt(Ma * 100) / 100; // media aritmetică a notelor, cu două zecimale exacte
var temp = Ma * 3 + tz, media_c = temp / 4; 
media_c = parseInt(media_c * 100) / 100; // media calculată, cu două zecimale exacte

Media şcolară ca formular independent

Medie dinamică
A tasta fără spaţiu.

medie =

<form>-ul alăturat conţine în principal următoarele elemente:

<input type="text" name = "note" onkeypress = "medie(this.form);"/>
<input type="text" name = "teza" onkeypress = "medie(this.form);"/>
<p>medie = <span> </span></p>

Tastarea unei note generează un eveniment 'keypress' care în acest context, provoacă execuţia funcţiei medie(); aceasta determină elementele implicate şi valorile acestora, apoi calculează şi expune "media şcolară" (sau NaN, dacă s-a tastat altceva decât o "notă").

În general, browserul "îngheaţă" pe perioada execuţiei unei funcţii javaScript (vorbind mai simplu - nu poate modifica documentul decât după terminarea execuţiei funcţiei); însă în cazul de faţă este necesară o sincronizare a evenimentelor, implicând funcţia javaScript setTimeout().

function medie(fobj) { // fobj referă un obiect <form> pentru note, teză, medie
    setTimeout(function() { 
        var note = fobj.note.value; // <input name="note" .../>
        var nr = note.length;
        var medie = fobj.getElementsByTagName('span')[0]; //<span> pentru medie

        var med = 0; // media şcolară de calculat
        
        for(var i = 0; i < note.length; i++) // determină suma notelor
            if(note.charAt(i) == "0") { // "0" corespunde numai notei "10"
                med += 9; // adunând 9 (în loc de "0") rezultă nota 10
                nr --;  // 10 s-a obţinut parcurgând două poziţii din şir
            }
            else med += parseInt(note.charAt(i));

        med = parseInt((med / nr)*100) / 100; // media aritmetică (două zecimale exacte)

        if(fobj.teza.value) {  
            teza = parseInt(fobj.teza.value); // <input name="teza" .../>
            med = (teza + 3 * med) / 4;
            med = parseInt(med * 100) / 100; // media calculată cu două zecimale exacte
        }

        medie.innerHTML = "" + med; // media oficială = Math.round(med);
    }, 10);
}

De observat că parametrul implicat - prin referinţă - este obiectul <form> (şi nu un atribut HTML al său) şi am evitat getElementById(); ne-am asigurat astfel posibilitatea de a "instanţia" formularul de medie şcolară de oricâte ori, în acelaşi document (atributul ID serveşte pentru a identifica unic un element, încât n-ar fi fost posibil în aceeaşi pagină, un al doilea formular cu aceeaşi funcţionalitate).

Însă un formular - şi "dichisit" - precum cel prezentat aici este potrivit să fie inserat într-un singur loc, în vederea unei utilizări conjuncturale. Pentru completarea mediilor într-un tabel mai amplu, de genul celui sugerat în preambulul articolului (şi se pot imagina desigur şi alte situaţii) - ar fi mai potrivit să putem invoca un obiect cât mai simplu, care să "încapă" cât mai firesc în oricare loc în care ar fi nevoie (şi numai cât timp ar fi necesar).

Media şcolară ca plugin jQuery

Să presupunem că avem un fişier index.html care produce la încărcarea în browser tabelul elevilor unei clase, având prima coloană deja completată cu nume şi prenume; avem de înscris a doua coloană, reprezentând mediile la un anumit obiect - de exemplu la Matematică, unde de obicei sunt mai multe note (plus teză) şi sunt mai multe variaţii (încât calculul mintal al mediei "cu două zecimale exacte" este mai degrabă riscant). Pentru asemenea sarcini este potrivit instrumentul pe care-l dezvoltăm mai jos.

Pentru început, înscriem în interiorul secţiunii <head> a fişierului index.html:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" src="mediasco.js"></script>

În acest fel, la încărcarea fişierului index.html browserul va accesa Google Hosted Libraries de unde va descărca (şi va păstra în cache) biblioteca javaScript jQuery; de asemenea, va încărca fişierul mediasco.js, presupus existent în aceeaşi locaţie (sau director) cu index.html.

Adăugăm în interiorul secţiunii <head>:

    <script>
      $(function() {
          $('.elev').click(function() { $(this).next().mediasco(); });
      });
    </script>

Acest script va fi executat imediat după încărcarea documentului "index.html" ($ este shortcut-ul instituit pentru funcţia jQuery(document).ready()) şi are următorul efect: pe fiecare element HTML cu atributul class="elev" - şi presupunem că elementele td corespunzătoare coloanei numelor au fost atributate astfel - se montează câte un handler de 'click' care la declanşare va instanţia funcţia mediasco(), presupusă existentă în fişierul mediasco.js. Aici $(this) returnează un obiect jQuery, corespunzător elementului care a declanşat evenimentul "click" (<td>-ul care conţine numele), iar $(this).next() returnează un obiect jQuery corespunzător primului "sibling" al acelui element (aici - următorul element <td>, cel corespunzător mediei).

Funcţia mediasco() va trebui să anexeze elementului <td> care a provocat-o, <input>-urile necesare pentru note şi medie (împreună cu funcţionalitatea de calcul şi expunere a mediei); se cuvine desigur, ca acestea să fie definite în exteriorul funcţiei mediasco() (drept "globale"), pentru a evita recrearea variabilelor respective la fiecare apăsare de tastă:

(function($) {
    var qht = []; // definiţiile HTML pentru cele trei elemente <input>
    qht.push('<span id="qmform" style="font-size:0.9em;">',
             ' Note:<input type="text" class="qinput" id="qnote" size="10"/>',
             ' Teza:<input type="text" class="qinput" id="qteza" size="2"/>',
             ' <input type="text" id="qok" value="OK" size="2" />',
             '</span>');
    function mediasco(element) { // referinţă la un obiect jQuery 
                                 // (asociat unui element HTML)
        element.after(qht.join('')); // adaugă (după elementul indicat) 
                                     // cele trei <input>-uri
        $('#qnote, #qteza').keyup(function() { // răspunde la o tastare de notă/teză
            // preia şirul de caractere din primul şi al doilea <input>
            var note = $('#qnote').val(), 
                teza = $('#qteza').val() || 0;
            var nr_car = note.length,  // Dacă nu avem o notă "10",
                nr_note = nr_car;      //   numărul de note = numărul de caractere  
            var m = 0; // media de calculat
            for(var i = 0; i < nr_car; i++) 
                if(note.charAt(i) == "0") { // "0" poate urma numai după "1" (nota "10")
                    m += 9;
                    nr_note --;
                }
                else m += parseInt(note.charAt(i));
            var med = parseInt((m / nr_note)*100) / 100; // media, cu două zecimale exacte
            if(teza) { // dacă există şi teză
                teza = parseInt(teza);
                med = (teza + 3 * med) / 4; 
                med = parseInt(med * 100) / 100; // media şcolară cu două zecimale exacte
            }
            element.html("<b>" + med + "</b>"); // expune media calculată
        });
        $('#qok').click(function() { // răspunde la click pe "OK"
            var med = element.text();
            element.html("<b>" + Math.round(med) + "</b>"); // media rotunjită
            $('#qmform').remove(); // elimină elementele adăugate
        });
    };
    $.fn.mediasco = function() { // extinde jQuery cu obiectul mediasco() (plug_in jQuery)
        $('#qmform').remove(); // elimină un eventual obiect #qmform rămas "neînchis"
        return mediasco(this);
    };
})(jQuery);

Pentru şablonul construcţiilor de genul celei de mai sus se poate vedea jQuery Plugins. Fişierul mediasco.js împreună cu un fişier index.html sunt furnizate în mediaScolara.zip.

Lămuriri particulare asupra formulei de plugin jQuery

Când întâlneşte o formulare ca (expresie), javaScript o "înlocuieşte" automat (dacă este corect constituită) cu rezultatul evaluării expresiei dintre paranteze; de exemplu
alert(n + " este " + ( n % 2 ? "impar" : "par" ) + "!")
va produce "23 este impar" sau "24 este par", după caz. Dar pentru un exemplu mai interesant, să înscriem în fişierul mediasco.js (însă în exteriorul scriptului reprodus mai sus):

( /* pentru iniţierea invocării unei funcţii */
    function() {
        var n = parseInt(Math.random() * 1000);
        alert(n + " este " + ( n % 2 ? "impar" : "par" ) + "!");
    }
) /* marchează sfârşitul invocării */
(); // operatorul de "apel a unei funcţii"

Reîncărcând în browser fişierul index.html (anume, prin CTRL + F5, ca să încarce "mediasco.js" tocmai modificat, în loc de a folosi copia veche din cache), se va afişa un mesaj de genul "541 este impar" - rezultat al evaluării imediate a funcţiei anonime încadrate între parantezele "de invocare".

Codul plugin-ului nostru este şi el ambalat în paranteze de invocare şi va fi executat imediat ce se va fi încărcat mediasco.js. Operatorul de apel din final are forma (jQuery) - ceea ce are ca efect o anumită contextualizare la spaţiul obiectului global jQuery, a funcţiei anonime invocate: simbolul $ care este transmis ca parametru funcţiei anonime invocate "intră" în spaţiul jQuery, primind în interiorul funcţiei semnificaţia prestabilită de alias sau "shortcut" pentru handlerul specific $(document).ready() (prin care, jQuery lansează funcţia indicată numai după ce documentul este "gata" pentru a putea fi citit (sau traversat) şi manipulat din javaScript, adică după ce browserul a constituit în memorie reprezentarea obiectuală a documentului curent).

Folosind $(document).ready() (sau scurtând, $), putem rescrie exemplul de mai sus astfel:

//( /* pentru iniţierea invocării unei funcţii */
$(document).ready(function() { // sau "scurt": $(function() {
    var n = parseInt(Math.random() * 1000);
    alert(n + " este " + ( n % 2 ? "impar" : "par" ) + "!");
});
//) /* marchează sfârşitul invocării */
//(); // operatorul de "apel a unei funcţii"

efectul reîncărcării "index.html" fiind acelaşi ca în exemplul iniţial: execuţia "imediată" a funcţiei anonime referite în .ready(), producând un mesaj de genul "541 este impar" (desigur, în exemplul iniţial "invocarea" se petrece mult mai devreme - nu după ce "documentul este gata", precum în a doua situaţie de invocare).

$(document).ready(function() {...}) răspunde evenimentului "obiectul document este gata", invocând atunci funcţia indicată. Înlocuind în această formulă document cu alte obiecte jQuery şi ready() cu o metodă proprie acestora - obţinem un comportament similar; de exemplu, în cazul nostru
$('#qnote, #qteza') . keyup(function() {...});
va răspunde evenimentului "obiectele indicate de qnote şi qteza există" şi va înregistra în consecinţă funcţia anonimă de executat la sesizarea ulterioară a unui eveniment "keyup" (analog cazului "ready"). Dar obiectele respective nu pot exista, decât prin execuţia funcţiei mediasco() care le creează şi le instanţiază în document; ori aceasta este una "privată", în sensul că este definită în contextul unei alte funcţii (anume, al funcţiei anonime invocate la încărcarea scriptului "mediasco.js") şi ca urmare, nu va putea fi apelată ca atare din exterior - ori aici avem nevoie să o apelăm din <head>-ul lui index.html, prin $(this).next().mediasco().

Această problemă s-a rezolvat în jQuery prin anexarea la obiectul jQuery a unui obiect special fn, astfel încât orice funcţie adăugată ca metodă a acestuia (în cazul nostru, prin $.fn.mediasco = function() {...}) să devină "publică" (dar numai în contextul obiectului jQuery, nu în spaţiul global window), adică să poată fi apelată ca atare din exteriorul funcţiei anonime care o conţine (dar numai dintr-un obiect din jQuery: fn nu este în window, ci este "sinonim" cu jQuery.prototype).

vezi Cărţile mele (de programare)

docerpro | Prev | Next