package pk.contender.earmouse;
import android.content.Context;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Keeps track of all the answers a user has given to exercises of a specific Module.
* <p>Provides methods for entering answers and obtaining useful information about this data and
* for reading and writing recorded statistical data to local storage.
*
* @author Paul Klinkenberg <pklinken.development@gmail.com>
*/
public class ModuleStats {
/** All the recorded answers */
private List<ModuleAnswer> moduleAnswerList;
/** Reference to local storage */
private final File statsFile;
/**
* Construct an instance to be associated with the given Module {@link Module#id}, attempts to load from local storage if the file
* exists, otherwise creates an fresh instance.
* @param context The application context, used for file operations
* @param id the ID of the Module this instance will be associated with
*/
public ModuleStats(Context context, int id) {
moduleAnswerList = new ArrayList<>();
File currentDir = context.getDir("files", Context.MODE_PRIVATE);
statsFile = new File(currentDir, "stats_" + id + ".json");
if (statsFile.exists()) {
FileReader fr = null;
try {
fr = new FileReader(statsFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if(fr != null) {
try {
initModuleStatsFromJson(fr);
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* Reads an existing ModuleStats instance from a JSON file
* @param fr The FileReader to read from
* @throws IOException
*/
private void initModuleStatsFromJson(FileReader fr) throws IOException {
JsonReader reader = new JsonReader(fr);
reader.beginArray();
while (reader.hasNext()) {
reader.beginObject();
int exerciseIndex = -1;
boolean result = true;
long timestamp = -1;
while (reader.hasNext()){
String name = reader.nextName();
switch (name) {
case "exerciseIndex":
exerciseIndex = reader.nextInt();
break;
case "result":
result = reader.nextBoolean();
break;
case "timestamp":
timestamp = reader.nextLong();
break;
default:
reader.skipValue();
break;
}
}
moduleAnswerList.add(new ModuleAnswer(exerciseIndex, result, timestamp));
reader.endObject();
}
reader.endArray();
reader.close();
}
/**
* Save moduleAnswerList to the given FileWriter as a JSON stream
* @param fw The FileWriter to save to
* @throws IOException
*/
private void saveModuleStatsToJson(FileWriter fw) throws IOException {
JsonWriter writer = new JsonWriter(fw);
writer.beginArray();
for (ModuleAnswer item : moduleAnswerList) {
writer.beginObject();
writer.name("exerciseIndex");
writer.value(item.getExerciseIndex());
writer.name("result");
writer.value(item.getResult());
writer.name("timestamp");
writer.value(item.getTimestamp());
writer.endObject();
}
writer.endArray();
writer.close();
}
/**
* Save this instance to local storage, if the file doesn't exist, creates it, otherwise overwrites existing data.
*/
public void saveModuleStats() {
if (!statsFile.exists()) {
try {
//noinspection ResultOfMethodCallIgnored
statsFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
FileWriter fw = null;
try {
fw = new FileWriter(statsFile);
} catch (IOException e) {
e.printStackTrace();
}
if(fw != null)
try {
saveModuleStatsToJson(fw);
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Add an answer to {@link #moduleAnswerList}
* @param exerciseIndex The index of the exercise the answer refers to
* @param result The correctness of the answer
*/
public void addAnswer(int exerciseIndex, boolean result) {
ModuleAnswer answer = new ModuleAnswer(exerciseIndex, result);
moduleAnswerList.add(answer);
}
/**
* Calculates the success rate of the registered answers.
* @return The percentage of registered answers that is correct or -1 if there is no available data.
*/
public int calculateSuccessRate() {
if(moduleAnswerList.size() == 0)
return -1;
else {
int correctAnswers = 0;
for (ModuleAnswer answer : moduleAnswerList) {
if (answer.getResult())
correctAnswers++;
}
return (int)(((float)correctAnswers / (float)moduleAnswerList.size()) * 100);
}
}
/**
* Returns the total amount of exercises completed
* <p>An exercise is completed when the user
* gave the correct answer, even if there were wrong answers before that.
* Since one moves on to the next exercise after giving a correct answer, the number of exercises completed
* is the same as the number of correct answers given.
* @return the number of exercises in this module that were answered correctly.
*/
public int exercisesCompleted() {
int result = 0;
for (ModuleAnswer answer: moduleAnswerList) {
if(answer.getResult())
result++;
}
return result;
}
/**
* Returns the success rate in % of a particular exerciseIndex, returns 0 if no exercises were found
* @param exerciseIndex the individual exercise whose success rate to return
* @return the success rate in % of the exercise with index exerciseIndex, or 0 if no records were found.
*/
public int exerciseSuccessRate(int exerciseIndex) {
int totalCount, correctCount;
totalCount = correctCount = 0;
for (ModuleAnswer answer : moduleAnswerList) {
if(answer.getExerciseIndex() == exerciseIndex) {
totalCount++;
if(answer.getResult())
correctCount++;
}
}
if(totalCount == 0)
return 0;
else
return (int)(((float)correctCount / (float)totalCount) * 100);
}
/**
* Returns the number of times exerciseIndex is registered.
* @return the number of times exerciseIndex is registered in {@link #moduleAnswerList}
*/
public int exerciseCount(int exerciseIndex) {
int result = 0;
// TODO: For the purpose of this function, perhaps it is better to only return the succesful answers, worth considering..
for (ModuleAnswer answer : moduleAnswerList) {
if(answer.getExerciseIndex() == exerciseIndex)
result++;
}
return result;
}
/**
* Delete all statistical data for this instance.
* @return True on success, false otherwise.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean purgeStats() {
moduleAnswerList = null;
return statsFile.exists() && statsFile.delete();
}
}