mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Add exporting scores and checkmarks files with multiple habits (#68).
Add writeMultipleHabits to write a scores file and a checkmarks file that contains scores and checkmarks of multiple habits. Add getTimeframe because it was necessary to get the timeframe between all habits so that row data could be populated correctly for habits that started before/after other habits. Move writeCheckmarks to below writeScores since it wasn't called until after writeScores in writeHabits. Add getByInterval to ScoreList, MemoryScoreList, and SQLiteScoreList to get scores between a given interval - simiarly to CheckmarkList. Add getValues (ScoreList) to get all values between a given timeframe. Add getNewest to get the newest repetition in a list and to correspond with getOldest. Add getDaysBetween to DateUtils to quickly get the number of days between two timestamps. Add tests for the new functions.
This commit is contained in:
@@ -75,6 +75,8 @@ public class HabitsCSVExporterTest extends BaseAndroidTest
|
||||
assertPathExists("001 Wake up early/Scores.csv");
|
||||
assertPathExists("002 Meditate/Checkmarks.csv");
|
||||
assertPathExists("002 Meditate/Scores.csv");
|
||||
assertPathExists("Checkmarks.csv");
|
||||
assertPathExists("Scores.csv");
|
||||
}
|
||||
|
||||
private void assertAbsolutePathExists(String s)
|
||||
|
||||
@@ -101,6 +101,30 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
||||
assertThat(records.get(0).timestamp, equalTo(today));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetByInterval()
|
||||
{
|
||||
long from = today - 10 * day;
|
||||
long to = today - 3 * day;
|
||||
|
||||
List<Score> list = scores.getByInterval(from, to);
|
||||
assertThat(list.size(), equalTo(8));
|
||||
|
||||
assertThat(list.get(0).getTimestamp(), equalTo(today - 3 * day));
|
||||
assertThat(list.get(3).getTimestamp(), equalTo(today - 6 * day));
|
||||
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetByInterval_withLongInterval()
|
||||
{
|
||||
long from = today - 200 * day;
|
||||
long to = today;
|
||||
|
||||
List<Score> list = scores.getByInterval(from, to);
|
||||
assertThat(list.size(), equalTo(201));
|
||||
}
|
||||
|
||||
private List<ScoreRecord> getAllRecords()
|
||||
{
|
||||
return new Select()
|
||||
@@ -109,5 +133,4 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
||||
.orderBy("timestamp desc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ public class HabitsCSVExporter
|
||||
private List<String> generateFilenames;
|
||||
|
||||
private String exportDirName;
|
||||
/**
|
||||
* Delimiter used in a CSV file.
|
||||
*/
|
||||
private final String DELIMITER = ",";
|
||||
|
||||
@NonNull
|
||||
private final HabitList allHabits;
|
||||
@@ -102,16 +106,6 @@ public class HabitsCSVExporter
|
||||
return s.substring(0, Math.min(s.length(), 100));
|
||||
}
|
||||
|
||||
private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks)
|
||||
throws IOException
|
||||
{
|
||||
String filename = habitDirName + "Checkmarks.csv";
|
||||
FileWriter out = new FileWriter(exportDirName + filename);
|
||||
generateFilenames.add(filename);
|
||||
checkmarks.writeCSV(out);
|
||||
out.close();
|
||||
}
|
||||
|
||||
private void writeHabits() throws IOException
|
||||
{
|
||||
String filename = "Habits.csv";
|
||||
@@ -134,6 +128,8 @@ public class HabitsCSVExporter
|
||||
writeScores(habitDirName, h.getScores());
|
||||
writeCheckmarks(habitDirName, h.getCheckmarks());
|
||||
}
|
||||
|
||||
writeMultipleHabits();
|
||||
}
|
||||
|
||||
private void writeScores(String habitDirName, ScoreList scores)
|
||||
@@ -146,6 +142,118 @@ public class HabitsCSVExporter
|
||||
out.close();
|
||||
}
|
||||
|
||||
private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks)
|
||||
throws IOException
|
||||
{
|
||||
String filename = habitDirName + "Checkmarks.csv";
|
||||
FileWriter out = new FileWriter(exportDirName + filename);
|
||||
generateFilenames.add(filename);
|
||||
checkmarks.writeCSV(out);
|
||||
out.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a scores file and a checkmarks file containing scores and checkmarks of every habit.
|
||||
* The first column corresponds to the date. Subsequent columns correspond to a habit.
|
||||
* Habits are taken from the list of selected habits.
|
||||
* Dates are determined from the oldest repetition date to the newest repetition date found in
|
||||
* the list of habits.
|
||||
*
|
||||
* @throws IOException if there was problem writing the files
|
||||
*/
|
||||
private void writeMultipleHabits() throws IOException
|
||||
{
|
||||
String scoresFileName = "Scores.csv";
|
||||
String checksFileName = "Checkmarks.csv";
|
||||
generateFilenames.add(scoresFileName);
|
||||
generateFilenames.add(checksFileName);
|
||||
FileWriter scoresWriter = new FileWriter(exportDirName + scoresFileName);
|
||||
FileWriter checksWriter = new FileWriter(exportDirName + checksFileName);
|
||||
|
||||
writeMultipleHabitsHeader(scoresWriter);
|
||||
writeMultipleHabitsHeader(checksWriter);
|
||||
|
||||
long[] timeframe = getTimeframe();
|
||||
long oldest = timeframe[0];
|
||||
long newest = DateUtils.getStartOfToday();
|
||||
|
||||
List<int[]> checkmarks = new ArrayList<>();
|
||||
List<int[]> scores = new ArrayList<>();
|
||||
for (Habit h : selectedHabits)
|
||||
{
|
||||
checkmarks.add(h.getCheckmarks().getValues(oldest, newest));
|
||||
scores.add(h.getScores().getValues(oldest, newest));
|
||||
}
|
||||
|
||||
int days = DateUtils.getDaysBetween(oldest, newest);
|
||||
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
|
||||
for (int i = 0; i <= days; i++)
|
||||
{
|
||||
Date day = new Date(newest - i * DateUtils.millisecondsInOneDay);
|
||||
|
||||
String date = dateFormat.format(day);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(date).append(DELIMITER);
|
||||
checksWriter.write(sb.toString());
|
||||
scoresWriter.write(sb.toString());
|
||||
|
||||
for(int j = 0; j < selectedHabits.size(); j++)
|
||||
{
|
||||
checksWriter.write(String.valueOf(checkmarks.get(j)[i]));
|
||||
checksWriter.write(DELIMITER);
|
||||
String score =
|
||||
String.format("%.4f", ((float) scores.get(j)[i]) / Score.MAX_VALUE);
|
||||
scoresWriter.write(score);
|
||||
scoresWriter.write(DELIMITER);
|
||||
}
|
||||
checksWriter.write("\n");
|
||||
scoresWriter.write("\n");
|
||||
}
|
||||
scoresWriter.close();
|
||||
checksWriter.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the first row, containing header information, using the given writer.
|
||||
* This consists of the date title and the names of the selected habits.
|
||||
*
|
||||
* @param out the writer to use
|
||||
* @throws IOException if there was a problem writing
|
||||
*/
|
||||
private void writeMultipleHabitsHeader(Writer out) throws IOException
|
||||
{
|
||||
out.write("Date" + DELIMITER);
|
||||
for (Habit h : selectedHabits) {
|
||||
out.write(h.getName());
|
||||
out.write(DELIMITER);
|
||||
}
|
||||
out.write("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the overall timeframe of the selected habits.
|
||||
* The timeframe is an array containing the oldest timestamp among the habits and the
|
||||
* newest timestamp among the habits.
|
||||
* Both timestamps are in milliseconds.
|
||||
*
|
||||
* @return the timeframe containing the oldest timestamp and the newest timestamp
|
||||
*/
|
||||
private long[] getTimeframe()
|
||||
{
|
||||
long oldest = Long.MAX_VALUE;
|
||||
long newest = -1;
|
||||
for (Habit h : selectedHabits)
|
||||
{
|
||||
if(h.getRepetitions().getOldest() == null || h.getRepetitions().getNewest() == null)
|
||||
continue;
|
||||
long currOld = h.getRepetitions().getOldest().getTimestamp();
|
||||
long currNew = h.getRepetitions().getNewest().getTimestamp();
|
||||
oldest = currOld > oldest ? oldest : currOld;
|
||||
newest = currNew < newest ? newest : currNew;
|
||||
}
|
||||
return new long[]{oldest, newest};
|
||||
}
|
||||
|
||||
private String writeZipFile() throws IOException
|
||||
{
|
||||
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
|
||||
|
||||
@@ -107,6 +107,16 @@ public abstract class RepetitionList
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Repetition getOldest();
|
||||
@Nullable
|
||||
/**
|
||||
* Returns the newest repetition in the list.
|
||||
* <p>
|
||||
* If the list is empty, returns null. Repetitions in the past are
|
||||
* discarded.
|
||||
*
|
||||
* @return newest repetition in the list, or null if list is empty.
|
||||
*/
|
||||
public abstract Repetition getNewest();
|
||||
|
||||
/**
|
||||
* Returns the total number of repetitions for each month, from the first
|
||||
|
||||
@@ -89,6 +89,46 @@ public abstract class ScoreList implements Iterable<Score>
|
||||
return s.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of scores that fall within the given interval.
|
||||
* <p>
|
||||
* There is exactly one score per day in the interval. The endpoints of
|
||||
* the interval are included. The list is ordered by timestamp (decreasing).
|
||||
* That is, the first score corresponds to the newest timestamp, and the
|
||||
* last score corresponds to the oldest timestamp.
|
||||
*
|
||||
* @param fromTimestamp timestamp of the beginning of the interval.
|
||||
* @param toTimestamp timestamp of the end of the interval.
|
||||
* @return the list of scores within the interval.
|
||||
*/
|
||||
@NonNull
|
||||
public abstract List<Score> getByInterval(long fromTimestamp,
|
||||
long toTimestamp);
|
||||
|
||||
/**
|
||||
* Returns the values of the scores that fall inside a certain interval
|
||||
* of time.
|
||||
* <p>
|
||||
* The values are returned in an array containing one integer value for each
|
||||
* day of the interval. The first entry corresponds to the most recent day
|
||||
* in the interval. Each subsequent entry corresponds to one day older than
|
||||
* the previous entry. The boundaries of the time interval are included.
|
||||
*
|
||||
* @param from timestamp for the oldest score
|
||||
* @param to timestamp for the newest score
|
||||
* @return values for the scores inside the given interval
|
||||
*/
|
||||
public final int[] getValues(long from, long to)
|
||||
{
|
||||
List<Score> scores = getByInterval(from, to);
|
||||
int[] values = new int[scores.size()];
|
||||
|
||||
for(int i = 0; i < values.length; i++)
|
||||
values[i] = scores.get(i).getValue();
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public List<Score> groupBy(DateUtils.TruncateField field)
|
||||
{
|
||||
computeAll();
|
||||
|
||||
@@ -92,6 +92,26 @@ public class MemoryRepetitionList extends RepetitionList
|
||||
return oldestRep;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Repetition getNewest()
|
||||
{
|
||||
long newestTime = -1;
|
||||
Repetition newestRep = null;
|
||||
|
||||
for (Repetition rep : list)
|
||||
{
|
||||
if (rep.getTimestamp() > newestTime)
|
||||
{
|
||||
newestRep = rep;
|
||||
newestTime = rep.getTimestamp();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return newestRep;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NonNull Repetition repetition)
|
||||
{
|
||||
|
||||
@@ -43,6 +43,21 @@ public class MemoryScoreList extends ScoreList
|
||||
(s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp()));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Score> getByInterval(long fromTimestamp, long toTimestamp)
|
||||
{
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
|
||||
List<Score> filtered = new LinkedList<>();
|
||||
|
||||
for (Score s : list)
|
||||
if (s.getTimestamp() >= fromTimestamp &&
|
||||
s.getTimestamp() <= toTimestamp) filtered.add(s);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Score getComputedByTimestamp(long timestamp)
|
||||
|
||||
@@ -125,6 +125,24 @@ public class SQLiteRepetitionList extends RepetitionList
|
||||
return record.toRepetition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repetition getNewest()
|
||||
{
|
||||
check(habit.getId());
|
||||
String query = "select habit, timestamp " +
|
||||
"from Repetitions " +
|
||||
"where habit = ? " +
|
||||
"order by timestamp desc " +
|
||||
"limit 1";
|
||||
|
||||
String params[] = { Long.toString(habit.getId()) };
|
||||
|
||||
RepetitionRecord record = sqlite.querySingle(query, params);
|
||||
if (record == null) return null;
|
||||
record.habit = habitRecord;
|
||||
return record.toRepetition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NonNull Repetition repetition)
|
||||
{
|
||||
|
||||
@@ -84,6 +84,29 @@ public class SQLiteScoreList extends ScoreList
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Score> getByInterval(long fromTimestamp, long toTimestamp)
|
||||
{
|
||||
check(habit.getId());
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
|
||||
String query = "select habit, timestamp, score " +
|
||||
"from Score " +
|
||||
"where habit = ? and timestamp >= ? and timestamp <= ? " +
|
||||
"order by timestamp desc";
|
||||
|
||||
String params[] = {
|
||||
Long.toString(habit.getId()),
|
||||
Long.toString(fromTimestamp),
|
||||
Long.toString(toTimestamp)
|
||||
};
|
||||
|
||||
List<ScoreRecord> records = sqlite.query(query, params);
|
||||
for (ScoreRecord record : records) record.habit = habitRecord;
|
||||
return toScores(records);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Score getComputedByTimestamp(long timestamp)
|
||||
@@ -127,11 +150,7 @@ public class SQLiteScoreList extends ScoreList
|
||||
List<ScoreRecord> records = sqlite.query(query, params);
|
||||
for (ScoreRecord record : records) record.habit = habitRecord;
|
||||
|
||||
List<Score> scores = new LinkedList<>();
|
||||
for (ScoreRecord rec : records)
|
||||
scores.add(rec.toScore());
|
||||
|
||||
return scores;
|
||||
return toScores(records);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -177,4 +196,12 @@ public class SQLiteScoreList extends ScoreList
|
||||
record.habit = habitRecord;
|
||||
return record.toScore();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<Score> toScores(@NonNull List<ScoreRecord> records)
|
||||
{
|
||||
List<Score> scores = new LinkedList<>();
|
||||
for (ScoreRecord r : records) scores.add(r.toScore());
|
||||
return scores;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,4 +272,18 @@ public abstract class DateUtils
|
||||
{
|
||||
MONTH, WEEK_NUMBER, YEAR, QUARTER
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of days between two timestamps (exclusively).
|
||||
*
|
||||
* @param t1 the first timestamp to use in milliseconds
|
||||
* @param t2 the second timestamp to use in milliseconds
|
||||
* @return the number of days between the two timestamps
|
||||
*/
|
||||
public static int getDaysBetween(long t1, long t2)
|
||||
{
|
||||
Date d1 = new Date(t1);
|
||||
Date d2 = new Date(t2);
|
||||
return (int) (Math.abs((d2.getTime() - d1.getTime()) / millisecondsInOneDay));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,27 @@ public class ScoreListTest extends BaseUnitTest
|
||||
assertThat(writer.toString(), equalTo(expectedCSV));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getValues()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
|
||||
long today = DateUtils.getStartOfToday();
|
||||
long day = DateUtils.millisecondsInOneDay;
|
||||
|
||||
long from = today - 4 * day;
|
||||
long to = today - 2 * day;
|
||||
|
||||
int[] expected = {
|
||||
11883254,
|
||||
11479288,
|
||||
11053198,
|
||||
};
|
||||
|
||||
int[] actual = habit.getScores().getValues(from, to);
|
||||
assertThat(actual, equalTo(expected));
|
||||
}
|
||||
|
||||
private void toggleRepetitions(final int from, final int to)
|
||||
{
|
||||
RepetitionList reps = habit.getRepetitions();
|
||||
|
||||
@@ -134,4 +134,14 @@ public class DateUtilsTest extends BaseUnitTest
|
||||
assertThat(DateUtils.truncate(field, t1), equalTo(expected));
|
||||
assertThat(DateUtils.truncate(field, t2), equalTo(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getDaysBetween()
|
||||
{
|
||||
long t1 = timestamp(2016, JANUARY, 1);
|
||||
long t2 = timestamp(2016, DECEMBER, 31);
|
||||
int expected = 365;
|
||||
assertThat(DateUtils.getDaysBetween(t1, t2), equalTo(expected));
|
||||
assertThat(DateUtils.getDaysBetween(t2, t1), equalTo(expected));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user