#!/usr/bin/perl -w
# Copyright (C) 2009 Tommaso Cucinotta
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# TODO
# x Use &Randomize() function in &Quiz() to avoid
# multiple asking the same word
# x Check german translations in %strings
# o Extract/show/save/train problem words
# x Show current vs. global statistics
#
my (%dir_dict, %frw_dict, %rev_dict);
my (%stat_scores, %stat_total);
my (%stat_part_scores, %stat_part_total);
my $language = "eng";
my $pw_mode = 0; # Problem Words mode
my $pw_treshold = 0.7; # Success rate below which a word becomes a PW
my $smart_update = 1; # Smart (weighted) update mode
my $update_weight = 0.66; # How much last test is weighted vs. previous
my $delay = 2; # Delay for ShowDictionary (-s option)
my $debug_mode = 0; # Debug mode
my ($dict_fname, $show_mode, $speak_mode, $clear_stats);
my %strings = (
Error => {
ger => "Nein: die richtig Antwort ist",
eng => "Error: the right answer is"},
Ok => {
ger => "Richt",
eng => "Right"},
ActualScore => {
ger => "Sein Teilskore ist",
eng => "Your partial score is"},
PreviousScore => {
ger => "Sein skore war",
eng => "Your score was"},
GlobalScore => {
ger => "Sein gesallt Skore ist",
eng => "Your global score is"},
ActualScorePW => {
ger => "Sein Teilskore mit Problemeworten ist",
eng => "Your partial score on problem words is"},
PreviousScorePW => {
ger => "Sein Skore mit Problemeworten war",
eng => "Your score on problem words was"},
GlobalScorePW => {
ger => "Sein gesallt Skore mit Problemeworten ist",
eng => "Your global score on problem words is"},
StoringStats => {
ger => "Statistik ist in",
eng => "Storing stats in"},
Retry => {
ger => "Sie haben ein zweite prove",
eng => "You have a second try"},
Usage => {
ger => "Verwendung",
eng => "Usage"},
PWFound => {
ger => "problematisch Wort gefunden",
eng => "problem words found"},
NoPWFound => {
ger => "Kein problematisch Wort gefunden",
eng => "No problem words found"},
);
my %speak_langs = (
eng => "en",
ger => "de",
ita => "it",
);
# Print a debug message on STDOUT if debug_mode is on.
sub DebugLog {
print @_ if ($debug_mode);
}
# Return a message according to the current language
sub Msg {
my ($key) = @_;
return "" . $strings{$key}{$language};
}
# Randomize an array
sub Randomize {
my @array = @_;
my @result;
DebugLog "Randomizing...\n";
while ($#array != -1) {
my $n = int(rand() * ($#array + 1));
DebugLog " " . $#array . ", $n\n";
push @result, $array[$n];
if ($n != $#array) {
$array[$n] = pop @array;
} else {
pop @array;
}
}
return @result;
}
# Remove any leading and trailing whitespaces
sub Reformat {
my ($ref) = @_;
$$ref =~ s/^\s*(.*)/$1/;
while ($$ref =~ /^(.*)\s+$/) {
$$ref = $1;
}
}
sub GetArticle {
my ($name) = @_;
if ($name =~ /(der|die|das).*/) {
return $1;
}
}
sub ArticleToGenre {
my ($name) = @_;
if ($name eq "der") {
return "n";
} elsif ($name eq "die") {
return "f";
} elsif ($name eq "das") {
return "n";
}
}
sub DumpDictionary {
foreach my $key (keys %dir_dict) {
DebugLog "$key = "
. join(", ", keys %{$dir_dict{$key}}) . "\n";
}
}
sub Delay {
my ($ticks) = @_;
while ($ticks > 0) {
$| = 1;
print ". ";
sleep(1);
$ticks--;
}
}
sub ShowDictionary {
my $i = 0;
my @keys = keys %dir_dict;
if ($pw_mode) {
@keys = &SelectPW(@keys);
if (! @keys) {
print &Msg("NoPWFound");
return;
}
}
@keys = &Randomize(@keys);
foreach my $key (@keys) {
$| = 1;
print "\n\n";
printf "%3d", int($i * 100 / $#keys);
print "% $key = ";
&Delay($delay);
print "" . join(", ", keys %{$dir_dict{$key}}) . "\n";
sleep(1);
$i++;
}
print "\n\n";
}
sub ShowDictionarySpeak {
my $i = 0;
my @keys = keys %dir_dict;
if ($pw_mode) {
@keys = &SelectPW(@keys);
if (! @keys) {
print &Msg("NoPWFound");
SayMessage("NoPWFound");
return;
}
}
@keys = &Randomize(@keys);
foreach my $key (@keys) {
$| = 1;
print "\n\n";
printf "%3d", int($i * 100 / $#keys);
print "% $key = ";
SayWord($key);
&Delay($delay);
my $str = join(", ", keys %{$dir_dict{$key}});
print "" . $str . "\n";
SayWordTranslation($key);
sleep(1);
$i++;
}
print "\n\n";
}
sub LoadDictionary {
my ($fname) = @_;
open(FD, "< $fname");
while ($line = ) {
# Catch command directives: '# --option = value'
# --lang =
if ($line =~ /\#[[:space:]]*--(.*)=(.*)/) {
my $opt = $1;
my $val = $2;
&Reformat(\$opt);
&Reformat(\$val);
$language = $val if ($opt eq "lang");
} elsif ($line =~ /(.*)=(.*)/) {
my @ita_words = split(/, /, $1);
my @eng_words = split(/, /, $2);
foreach my $eng_word (@eng_words) {
foreach my $ita_word (@ita_words) {
&Reformat(\$eng_word);
&Reformat(\$ita_word);
if ($eng_word && $ita_word) {
$dir_dict{$eng_word}{$ita_word} = 1;
$dir_dict{$ita_word}{$eng_word} = 1;
$frw_dict{$ita_word}{$eng_word} = 1;
$rev_dict{$eng_word}{$ita_word} = 1;
}
}
}
}
}
close(FD);
}
sub CreateStatistics {
foreach my $key (keys %dir_dict) {
$stat_scores{$key} = 0;
$stat_total{$key} = 0;
$stat_part_scores{$key} = 0;
$stat_part_total{$key} = 0;
}
# foreach my $key (keys %rev_dict) {
# $stat_scores{$key} = 0;
# $stat_total{$key} = 0;
# }
}
sub LoadStatistics {
my ($fname) = @_;
if (-f $fname) {
open(FD, "< $fname");
while ($line = ) {
if ($line =~ /(.*)=(.*)\/(.*)/) {
$stat_scores{$1} = $2;
$stat_total{$1} = $3;
}
}
close(FD);
}
}
sub WriteStatistics {
my ($fname) = @_;
open(FD, "> $fname");
foreach my $key (keys %stat_scores) {
print FD "$key=" . $stat_scores{$key} . "/" . $stat_total{$key} . "\n";
}
close(FD);
}
# my $score = &CalcStatistics(\%stat_scores, \%stat_total, keys %stat_scores);
sub CalcStatistics {
my ($stat_scores, $stat_total, @keys) = @_;
my $score = 0;
my $total = 0;
DebugLog "CalcStatistics:\n";
foreach my $key (@keys) {
DebugLog " $key: " . $stat_scores->{$key} . "/"
. $stat_total->{$key} . "\n";
$score += $stat_scores->{$key} if $stat_scores->{$key};
$total += $stat_total->{$key} if $stat_total->{$key};
}
if ($total == 0) {
$score = 0;
} else {
$score = int($score * 100 / $total);
}
DebugLog " Global Score = $score\n";
return $score;
}
sub UpdateStatistics {
foreach my $key (keys %stat_part_scores) {
my $ti = $stat_total{$key}; # t[i]
my $ri = $stat_scores{$key}; # r[i]
my $pti1 = $stat_part_total{$key}; # pt[i+1]
my $pri1 = $stat_part_scores{$key}; # pr[i+1]
my ($ti1, $ri1); # t[i+1], r[i+1]
my $uw = $update_weight;
$ti1 = $ti + $pti1;
if (! $smart_update) {
# Normal update:
$ri1 = $ri + $pri1;
} else {
# Smart update: last test has a weight of $alpha, where all
# of the previous ones have a weight of (1-$alpha)
if ($pti1 == 0) {
$ri1 = $ri;
} else {
if ($ti == 0) {
# No previous tests, so the last gets full score (no weight)
$ri1 = $ti1 * $pri1/$pti1;
} else {
$ri1 = $ti1 * ((1-$uw)*$ri/$ti + $uw*$pri1/$pti1);
}
}
}
$stat_total{$key} = $ti1;
$stat_scores{$key} = $ri1;
}
}
sub DumpStatistics {
my @keys = keys %dir_dict;
my $pw_str = "";
if ($pw_mode) {
@keys = &SelectPW(@keys);
$pw_str = "PW";
}
my $score = &CalcStatistics(\%stat_scores, \%stat_total, @keys);
print "\n" . &Msg("PreviousScore" . $pw_str) . " $score%\n";
$score = &CalcStatistics(\%stat_part_scores, \%stat_part_total, @keys);
print "\n" . &Msg("ActualScore" . $pw_str) . " $score%\n";
&UpdateStatistics();
# Now the statistics after update are calculated on THE SAME KEYS
# as before updating
$score = &CalcStatistics(\%stat_scores, \%stat_total, @keys);
print "\n" . &Msg("GlobalScore" . $pw_str) . " $score%\n";
if ($pw_mode) {
$score = &CalcStatistics(\%stat_scores, \%stat_total, keys %stat_scores);
print "\n" . &Msg("GlobalScore") . " $score%\n";
}
}
sub GetLine {
my ($question) = @_;
print "$question\n";
my $answer = "" . ;
chop $answer;
&Reformat(\$answer);
return $answer;
}
sub Match {
my ($a, $b) = @_;
if (exists $dir_dict{$a}{$b}) {
return 1;
} else {
return 0;
}
}
sub SelectPW {
my @keys = @_;
# Eliminate good guessed keys, that is keys with success rate >= pw_treshold
my $i = 0;
while ($i <= $#keys) {
my $key = $keys[$i];
if (($stat_total{$key} > 0)
&& ($stat_scores{$key} / $stat_total{$key} >= $pw_treshold)) {
splice(@keys, $i, 1);
} else {
$i++;
}
}
return @keys;
}
sub SayMessage {
my ($msg) = @_;
if ($speak_mode && exists $speak_langs{$language}) {
my $cmd = "espeak -v " . $speak_langs{$language} . ' "' . &Msg($msg) . '"';
my $out = `$cmd`;
}
}
sub SayWord {
my ($word) = @_;
if ($speak_mode && exists $speak_langs{$language}) {
if (exists $rev_dict{$word}) {
$speak_lang = $speak_langs{$language};
} else {
$speak_lang = "it";
}
my $cmd = "espeak -v " . $speak_lang . ' "' . $word . '"';
my $out = `$cmd`;
}
}
sub SayWordTranslation {
my ($word) = @_;
my ($speak_lang, $str);
if (exists $frw_dict{$word}) {
$str = join(", ", keys %{$frw_dict{$word}});
$speak_lang = $speak_langs{$language};
} else {
$str = join(", ", keys %{$rev_dict{$word}});
$speak_lang = "it";
}
my $cmd = "espeak -v " . $speak_lang . ' "' . $str . '"';
my $out = `$cmd`;
}
sub Quiz {
my @keys = keys %dir_dict;
if ($pw_mode) {
# Eliminate good guessed keys
@keys = &SelectPW(@keys);
if (@keys) {
print "" . ($#keys + 1) . " " . &Msg("PWFound") . "\n";
} else {
print &Msg("NoPWFound") . "\n";
return;
}
}
@keys = &Randomize(@keys);
my $num_keys = $#keys + 1;
my $num = 1;
foreach my $rnd_key (@keys) {
my $question = sprintf("[%02d%%] -> $rnd_key ?", $num*100/$num_keys);
SayWord($rnd_key);
my $answer = &GetLine($question);
if ("$answer" =~ /^(q|quit|exit|x)$/i) {
return;
} elsif (&Match($rnd_key, $answer)) {
$stat_part_scores{$rnd_key}++;
print "\t\t" . &Msg("Ok") . "\n";
SayMessage("Ok");
} else {
SayMessage("Retry");
SayWord($rnd_key);
$answer = &GetLine(&Msg("Retry") . " -> $rnd_key ?");
if (&Match($rnd_key, $answer)) {
$stat_part_scores{$rnd_key}++;
print "\t\t" . &Msg("Ok") . "\n";
SayMessage("Ok");
} else {
SayMessage("Error");
SayWordTranslation($rnd_key);
print "\t\t" . &Msg("Error") . " "
. join(", ", keys %{$dir_dict{$rnd_key}}) . "\n";
}
}
$stat_part_total{$rnd_key}++;
$num++;
}
}
sub Usage {
print &Msg(Usage) . ": WordTeacher.pl [options] \\n";
print " Options:\n";
print " --help|-h Show this help screen and exit\n";
print " --show|-s Show only dictionary, no quiz\n";
print " --speak|-p Enable use of espeak engine\n";
print " --delay|-d Set show delay\n";
print " --clear-stats|-cs Clear statistics\n";
print " -l Set program language\n";
print " --problem-words|-pw Train only problem words\n";
print " -pwt Set problem word treshold\n";
print " -uw Set update weight (default = 0.66)\n";
print " --normal-update|-nu Disable weighted stat update\n";
print " --debug-mode|-dm Enable debug mode\n";
}
my $i = 0;
while ($i <= $#ARGV) {
if (($ARGV[$i] eq "--help") || ($ARGV[$i] eq "-h")) {
Usage();
exit 0;
} elsif (($ARGV[$i] eq "--show") || ($ARGV[$i] eq "-s")) {
$show_mode = 1;
} elsif (($ARGV[$i] eq "--speak") || ($ARGV[$i] eq "-p")) {
$speak_mode = 1;
} elsif (($ARGV[$i] eq "--debug-mode") || ($ARGV[$i] eq "-dm")) {
$debug_mode = 1;
} elsif (($ARGV[$i] eq "--problem-words") || ($ARGV[$i] eq "-pw")) {
$pw_mode = 1;
} elsif (($ARGV[$i] eq "--clear-stats") || ($ARGV[$i] eq "-cs")) {
$clear_stats = 1;
} elsif (($ARGV[$i] eq "--lang") || ($ARGV[$i] eq "-l")) {
$i++;
$language = $ARGV[$i];
} elsif (($ARGV[$i] eq "--delay") || ($ARGV[$i] eq "-d")) {
$i++;
$delay = $ARGV[$i];
} elsif ($ARGV[$i] eq "-pwt") {
$i++;
$pw_treshold = $ARGV[$i];
} elsif (($ARGV[$i] eq "--update-weight") || ($ARGV[$i] eq "-uw")) {
$i++;
$update_weight = $ARGV[$i];
} elsif (($ARGV[$i] eq "--normal-update") || ($ARGV[$i] eq "-nu")) {
$smart_update = 0;
} else {
$dict_fname = $ARGV[$i];
}
$i++;
}
&LoadDictionary($dict_fname);
my $stat_fname = $dict_fname;
$stat_fname =~ s/\.dict/\.stat/;
print "" . &Msg("StoringStats") . " $stat_fname\n";
&DumpDictionary();
&CreateStatistics();
&LoadStatistics($stat_fname) if (! $clear_stats);
if ($show_mode) {
&ShowDictionarySpeak();
exit(0);
}
&Quiz();
&DumpStatistics();
&WriteStatistics($stat_fname);