Τεχνικό Άρθρο

Παρακολούθηση Αξιολόγησης Τύπων Excel Βήμα προς Βήμα στη Delphi

Το Excel κρύβει ένα μικρό πρόγραμμα αποσφαλμάτωσης (debugger) σε κοινή θέα. Επιλέξτε ένα κελί, ανοίξτε τους Τύπους (Formulas) και κάντε κλικ στην Αξιολόγηση Τύπου (Evaluate Formula), και ένα παράθυρο διαλόγου εμφανίζει τον τύπο με μια υποέκφραση υπογραμμισμένη. Πατήστε Αξιολόγηση και αυτή η υποέκφραση συμπτύσσεται στην τιμή της, στη συνέχεια υπογραμμίζεται η επόμενη, και παρακολουθείτε μια μεγάλη έκφραση να συρρικνώνεται σε έναν μόνο αριθμό, μία αναγωγή (reduction) τη φορά. Είναι ο πιο γρήγορος τρόπος για να βρείτε ποιος κλάδος μιας ένθετης συνάρτησης IF ενεργοποιήθηκε πραγματικά, ή ποια αναφορά τροφοδότησε ένα λάθος σύνολο. Το HotXLS αναπαράγει ακριβώς αυτή τη συμπεριφορά μέσω του TXLSFormulaTracer, ώστε ένα πρόγραμμα Delphi ή C++Builder να μπορεί να αποδώσει την ίδια λίστα βημάτων για τον έλεγχο ενός βιβλίου εργασίας, την αποσφαλμάτωση ενός παραγόμενου τύπου, ή για να διδάξει σε κάποιον γιατί ένα αποτέλεσμα βγήκε με αυτόν τον τρόπο. Κάθε καταγεγραμμένο βήμα μεταφέρει το κείμενο της υποέκφρασης και την τιμή στην οποία ανάγεται

Πώς η μηχανή αναγωγής διατρέχει την έκφραση

Ο ιχνηλάτης (tracer) δεν εισχωρεί στη μηχανή υπολογισμού. Μετατρέπει τον τύπο σε διακριτά στοιχεία (tokenizes) και τον αναλύει με έναν αναλυτή (parser) αναδρομικής κατάβασης (recursive-descent), στη συνέχεια ανάγει το δέντρο κατά βάθος πρώτα (depth-first), ξεκινώντας από την πιο εσωτερική αξιολογήσιμη υποέκφραση. Όταν ένας κόμβος ανάγεται σε μια τιμή, αυτή η τιμή αντικαθίσταται πίσω στην περιβάλλουσα έκφραση ως κυριολεκτικό (literal), και η μηχανή ζητά από τον πραγματικό υπολογιστή να επανυπολογίσει την πλέον απλούστερη έκφραση. Επειδή κάθε βήμα αξιολογείται μέσω της δημόσιας μεθόδου Calculate του υπολογιστικού φύλλου και όχι με κάποια ιδιωτική συντόμευση, κάθε βήμα συμφωνεί απόλυτα με αυτό που θα παρήγαγε ένας πλήρης επανυπολογισμός του κελιού. Ο αναλυτής είναι μη παρεμβατικός εκ σχεδιασμού, πράγμα που του επιτρέπει να εκτελείται σε οποιοδήποτε υπολογιστικό φύλλο χωρίς να διαταράσσει την κατάστασή του

Ο αναλυτής ακολουθεί μια κλίμακα προτεραιότητας τελεστών, με ένα αναδρομικό επίπεδο ανά ζώνη προτεραιότητας. Από τη χαμηλότερη δέσμευση προς την υψηλότερη, οι ζώνες είναι: επίπεδο 0 σύγκριση (=, <>, <, >, <=, >=), επίπεδο 1 συνένωση συμβολοσειρών (&), επίπεδο 2 πρόσθεση και αφαίρεση, επίπεδο 3 πολλαπλασιασμός και διαίρεση, επίπεδο 4 ύψωση σε δύναμη, και τέλος μοναδιαίο (unary) συν και πλην κάτω από αυτό. Κάθε επίπεδο αναλύει το επίπεδο πάνω από αυτό για τους τελεστές του, επομένως μια υψηλότερη ζώνη δεσμεύει πιο σφιχτά. Αυτή είναι η ίδια προτεραιότητα που εφαρμόζει το Excel, γι' αυτό το A1*B1+A2*B1 ανάγει τα δύο γινόμενα πριν από το άθροισμα: ο πολλαπλασιασμός βρίσκεται στο επίπεδο 3, η πρόσθεση στο επίπεδο 2, επομένως οι πολλαπλασιασμοί είναι βαθύτερα στο δέντρο και ανάγονται πρώτοι

Παρακολούθηση ενός τύπου και διαδρομή των βημάτων

Η χρήση αντικατοπτρίζει την παρεχόμενη επίδειξη (demo) στο Demo/Delphi/FormulaTrace/FormulaTrace.dpr. Δημιουργήστε ένα υπολογιστικό φύλλο (ή ανοίξτε ένα υπάρχον βιβλίο εργασίας), κατασκευάστε έναν ιχνηλάτη πάνω από το φύλλο, καλέστε το Trace και επαναλάβετε (iterate) τον πίνακα που επιστρέφεται. Κάθε TXLSFormulaStep εκθέτει το Depth για εσοχή, το Source για την αρχική υποέκφραση, το Expression για αυτήν την υποέκφραση με τους τελεστές της ήδη αντικατεστημένους, και το Value για το αποτέλεσμα του βήματος

uses
  SysUtils, Variants, lxHandle, lxHandleX, lxFormulaTrace;

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Tracer: TXLSFormulaTracer;
  Steps: TXLSFormulaStepArray;
  Final: Variant;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Order');
    Sheet.Cells[1, 1].Value := 10;    // A1 units
    Sheet.Cells[1, 2].Value := 25;    // B1 unit price
    Sheet.Cells[1, 3].Value := 0.08;  // C1 tax rate

    Tracer := TXLSFormulaTracer.Create(Sheet);
    try
      Final := Tracer.Trace('A1*B1*(1+C1)', Steps);
      for I := 0 to High(Steps) do
        Writeln(StringOfChar(' ', Steps[I].Depth * 2),
                Steps[I].Source, ' -> ', Steps[I].Expression,
                ' = ', VarToStr(Steps[I].Value));
      Writeln('result = ', VarToStr(Final));
    finally
      Tracer.Free;
    end;
  finally
    Book.Free;
  end;
end;

Οι αναφορές κελιών επιλύονται πρώτες και εμφανίζονται ως δικά τους βήματα, στη συνέχεια ανάγονται τα γινόμενα, μετά ο φορολογικός συντελεστής σε παρένθεση, και ο τελικός πολλαπλασιασμός κλείνει τη διαδικασία. Το πεδίο Depth σας επιτρέπει να κάνετε εσοχή ώστε οι πιο εσωτερικές αναγωγές να κάθονται εμφανώς πιο βαθιά, ακριβώς όπως το Excel υπογραμμίζει τον πιο εσωτερικό όρο πριν από οποιονδήποτε εξωτερικό

Η παγίδα του κυριολεκτικού χωρίς περιοχή (locale-free)

Η πιο επικίνδυνη λεπτομέρεια σε όλο αυτό το σχήμα είναι αόρατη σε ένα αγγλικό μηχάνημα και σπάει θορυβωδώς σε ένα γερμανικό. Όταν ένας υπολογισμένος αριθμός αντικαθίσταται πίσω στο κείμενο του τύπου, πρέπει να γραφτεί ως συμβολοσειρά και στη συνέχεια να αναλυθεί ξανά από τη μηχανή υπολογισμού, η οποία αντιμετωπίζει το . ως υποδιαστολή. Αν η αντικατάσταση χρησιμοποιούσε τις τοπικές ρυθμίσεις (locale) του συστήματος, ένα γερμανικό TFormatSettings θα έγραφε 1,08 για τον φορολογικό συντελεστή, το κόμμα θα διαβαζόταν ως διαχωριστικό ορισμάτων, και ο επανυπολογισμός του A1*B1*1,08 είτε θα αναλυόταν σε λάθος μορφή είτε θα αποτύγχανε εντελώς

Ο ιχνηλάτης το αποφεύγει αυτό μορφοποιώντας κάθε αριθμητικό κυριολεκτικό μέσω ενός ιδιωτικού TFormatSettings που καρφιτσώνει (pins) κατά την κατασκευή, με το DecimalSeparator αναγκασμένο σε . και το ThousandSeparator ορισμένο σε #0 ώστε να μην εκπέμπεται ποτέ χαρακτήρας ομαδοποίησης. Η FloatToStr παράγει στη συνέχεια ένα κυριολεκτικό που η μηχανή μπορεί πάντα να διαβάσει πίσω, ανεξάρτητα από τις τοπικές ρυθμίσεις (regional settings) του χειριστή

// Conceptually what the tracer pins once, at construction
FFloatFmt := FormatSettings;
FFloatFmt.DecimalSeparator := '.';
FFloatFmt.ThousandSeparator := #0;
// every reduced number is written with: FloatToStr(Double(V), FFloatFmt)

Αυτό είναι το είδος του σφάλματος που δεν εμφανίζεται ποτέ στις δοκιμές του ίδιου του δημιουργού και έρχεται στην επιφάνεια μόνο όταν ένας πελάτης σε άλλη περιοχή (locale) εκτελεί τον ίδιο κώδικα, επομένως αξίζει να δηλωθεί ξεκάθαρα: η αμφίδρομη μετάβαση (round-tripping) μιας τιμής μέσω κειμένου τύπου είναι ένα πρόβλημα σειριοποίησης (serialization), και η σειριοποίηση πρέπει να είναι ανεξάρτητη από τις τοπικές ρυθμίσεις (locale-free)

Τα Boolean ανάγονται σε 1 και 0

Μια σχετική απόφαση αντικατάστασης αφορά τις λογικές τιμές. Όταν μια υποέκφραση αξιολογείται σε ένα boolean, ο ιχνηλάτης το γράφει πίσω ως 1 ή 0, όχι ως TRUE ή FALSE. Ο λόγος είναι ότι το ανηγμένο κυριολεκτικό πρέπει να αναλυθεί ξανά καθαρά σε οποιοδήποτε περιβάλλον (context) το περιβάλλει, και η αριθμητική είναι η απαιτητική περίπτωση. Αν μια σύγκριση όπως A1>A2 αναγόταν στο κείμενο TRUE και αυτό το κείμενο κατέληγε μέσα σε TRUE*B1, ο επανυπολογισμός θα εξαρτιόταν από το εάν η μηχανή αποδέχεται μια σκέτη λέξη-κλειδί boolean σε έναν πολλαπλασιασμό. Η αντικατάσταση του 1 παρακάμπτει το ερώτημα εντελώς, επειδή το 1*B1 είναι ξεκάθαρο σε οποιαδήποτε αριθμητική θέση. Ταιριάζει επίσης με τον δικό του εξαναγκασμό (coercion) του Excel, όπου το TRUE συμπεριφέρεται ως 1 και το FALSE ως 0 τη στιγμή που αναμένεται ένας αριθμός

Οι κλήσεις συναρτήσεων ανάγονται ατομικά

Μια απλοϊκή μηχανή βημάτων θα ανήγαγε πρώτα τα ορίσματα μιας συνάρτησης και μετά την κλήση. Αυτό είναι λάθος για το Excel, και ο ιχνηλάτης εσκεμμένα δεν το κάνει. Μια κλήση συνάρτησης αξιολογείται ως σύνολο, από το αρχικό της κείμενο, σε ένα μόνο βήμα. Ο λόγος είναι η σημασιολογία βραχυκυκλώματος (short-circuit semantics). Tα IF, CHOOSE και IFERROR αξιολογούν μόνο τον κλάδο που επιλέγουν, και η αναγωγή των ορισμάτων πρώτα θα ανάγκαζε τη μηχανή να υπολογίσει κλάδους που το Excel δεν αγγίζει ποτέ. Το κλασικό θύμα είναι μια δικλείδα προστασίας από διαίρεση με το μηδέν, όπως το IF(B1=0,0,A1/B1): αν ο ιχνηλάτης ανήγαγε το A1/B1 πριν αξιολογήσει το IF, η δικλείδα θα αποτύγχανε και θα προκαλούσε ακριβώς το σφάλμα που υπάρχει για να αποτρέψει. Αξιολογώντας ολόκληρη την κλήση ατομικά (atomically), ο ιχνηλάτης διατηρεί την οκνηρή αξιολόγηση (lazy evaluation) που κάνει τέτοιες δικλείδες να λειτουργούν

// IF is one atomic step; only the selected branch is evaluated
Final := Tracer.Trace('IF(A1>A2,A1*B1,A2*B1)', Steps);
// A1>A2 is true, so the step records A1*B1 as the chosen result;
// A2*B1 is never computed, exactly as Excel would do it.

Ο συμβιβασμός είναι ότι δεν βλέπετε μέσα στην κλήση της συνάρτησης ως ξεχωριστά βήματα, αλλά αυτή είναι η σωστή συμπεριφορά. Η εμφάνιση αναγωγών ορισμάτων που το Excel δεν εκτελεί ποτέ θα ήταν μια πιο παραπλανητική παρακολούθηση (trace) από το να αντιμετωπίζεται η κλήση ως η ενιαία μονάδα αξιολόγησης που πραγματικά είναι

Διαχωριστικά ορισμάτων και άθικτες περιοχές

Δύο ακόμη κανονικοποιήσεις (normalizations) διατηρούν τον επανυπολογισμό ειλικρινή. Ο μεταγλωττιστής (compiler) της μηχανής υπολογισμού αναμένει το ; ως διαχωριστικό ορισμάτων συνάρτησης, επομένως, όταν ο ιχνηλάτης ανακατασκευάζει μια κλήση συνάρτησης από το αναλυμένο του δέντρο, ενώνει τα ορίσματα με ;, ακόμα κι αν ο χρήστης πληκτρολόγησε αρχικά ,. Ένας τύπος γραμμένος ως SUM(A1,A2,A3) επανυπολογίζεται ως SUM(A1;A2;A3), το οποίο η μηχανή αποδέχεται. Η αντικατάσταση των τιμών είναι αυτή που καθιστά απαραίτητη αυτήν την ανακατασκευή, και η σωστή επιλογή του διαχωριστικού είναι αυτή που κάνει την ανακατασκευή να αναλύεται σωστά

Οι αναφορές περιοχών (range references) είναι η άλλη περίπτωση. Μια περιοχή όπως το A1:A3 δεν είναι ένα βαθμωτό μέγεθος (scalar) και δεν πρέπει να χωριστεί σε τρεις ξεχωριστές τιμές, επειδή η συνάρτηση που την καταναλώνει αναμένει ένα όρισμα περιοχής. Ο ιχνηλάτης διατηρεί μια περιοχή άθικτη ως το αρχικό της κείμενο και αφήνει την περιβάλλουσα συνάρτηση να αναχθεί ως σύνολο. Στο SUM(A1:A3)*B1 η περιοχή παραμένει ολόκληρη, το SUM(A1:A3) ανάγεται σε έναν αριθμό σε ένα ατομικό βήμα, και μόνο τότε εκτελείται ο εξωτερικός πολλαπλασιασμός. Αυτό είναι το ίδιο όριο που χαράσσει το Excel μεταξύ ενός τελεστή περιοχής (range operand) και του βαθμωτού μεγέθους που τελικά συνεισφέρει

// The range A1:A3 is never split; SUM is one atomic reduction,
// then the product with B1 reduces on top of it.
Final := Tracer.Trace('SUM(A1:A3)*B1', Steps);
for I := 0 to High(Steps) do
  Writeln(Steps[I].Source, ' = ', VarToStr(Steps[I].Value));

Συνολικά, αυτοί οι κανόνες κάνουν τη λίστα βημάτων έναν πιστό καθρέφτη της εντολής Αξιολόγηση Τύπου του Excel και όχι μια προσέγγισή της. Οι αναγωγές συμβαίνουν με τη σειρά που τις εκτελεί το Excel, τα αντικατεστημένα κυριολεκτικά επιβιώνουν σε οποιεσδήποτε τοπικές ρυθμίσεις (locale), τα booleans εξαναγκάζονται (coerce) με τον τρόπο που τα εξαναγκάζει το Excel, και οι οκνηρές συναρτήσεις (lazy functions) παραμένουν οκνηρές. Αν θέλετε να ωθήσετε τη μηχανή περαιτέρω με τις δικές σας συναρτήσεις, το άρθρο για τη μηχανή τύπων και τις προσαρμοσμένες συναρτήσεις δείχνει πώς να τις καταχωρήσετε, και για πιο βαριά αριθμητική εργασία, το άρθρο για τις συναρτήσεις στατιστικής κατανομής στη Delphi καλύπτει την ενσωματωμένη βιβλιοθήκη με την οποία αξιολογεί ο ιχνηλάτης. Όλα αυτά αποστέλλονται ως μέρος του στοιχείου υπολογιστικού φύλλου HotXLS για Delphi και C++Builder, μαζί με τα API ανάγνωσης, εγγραφής, μορφοποίησης και υπολογισμού που καλύπτονται αλλού σε αυτό το ιστολόγιο