mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Merge pull request #173 from JotraN/dev
Add exporting scores and checkmarks files with multiple habits (#68).
This commit is contained in:
@@ -75,6 +75,8 @@ public class HabitsCSVExporterTest extends BaseAndroidTest
|
|||||||
assertPathExists("001 Wake up early/Scores.csv");
|
assertPathExists("001 Wake up early/Scores.csv");
|
||||||
assertPathExists("002 Meditate/Checkmarks.csv");
|
assertPathExists("002 Meditate/Checkmarks.csv");
|
||||||
assertPathExists("002 Meditate/Scores.csv");
|
assertPathExists("002 Meditate/Scores.csv");
|
||||||
|
assertPathExists("Checkmarks.csv");
|
||||||
|
assertPathExists("Scores.csv");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertAbsolutePathExists(String s)
|
private void assertAbsolutePathExists(String s)
|
||||||
|
|||||||
@@ -101,6 +101,30 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
|||||||
assertThat(records.get(0).timestamp, equalTo(today));
|
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()
|
private List<ScoreRecord> getAllRecords()
|
||||||
{
|
{
|
||||||
return new Select()
|
return new Select()
|
||||||
@@ -109,5 +133,4 @@ public class SQLiteScoreListTest extends BaseAndroidTest
|
|||||||
.orderBy("timestamp desc")
|
.orderBy("timestamp desc")
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ public class HabitsCSVExporter
|
|||||||
private List<String> generateFilenames;
|
private List<String> generateFilenames;
|
||||||
|
|
||||||
private String exportDirName;
|
private String exportDirName;
|
||||||
|
/**
|
||||||
|
* Delimiter used in a CSV file.
|
||||||
|
*/
|
||||||
|
private final String DELIMITER = ",";
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final HabitList allHabits;
|
private final HabitList allHabits;
|
||||||
@@ -102,16 +106,6 @@ public class HabitsCSVExporter
|
|||||||
return s.substring(0, Math.min(s.length(), 100));
|
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
|
private void writeHabits() throws IOException
|
||||||
{
|
{
|
||||||
String filename = "Habits.csv";
|
String filename = "Habits.csv";
|
||||||
@@ -134,6 +128,8 @@ public class HabitsCSVExporter
|
|||||||
writeScores(habitDirName, h.getScores());
|
writeScores(habitDirName, h.getScores());
|
||||||
writeCheckmarks(habitDirName, h.getCheckmarks());
|
writeCheckmarks(habitDirName, h.getCheckmarks());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeMultipleHabits();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeScores(String habitDirName, ScoreList scores)
|
private void writeScores(String habitDirName, ScoreList scores)
|
||||||
@@ -146,6 +142,118 @@ public class HabitsCSVExporter
|
|||||||
out.close();
|
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
|
private String writeZipFile() throws IOException
|
||||||
{
|
{
|
||||||
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
|
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
|
||||||
|
|||||||
@@ -107,6 +107,16 @@ public abstract class RepetitionList
|
|||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public abstract Repetition getOldest();
|
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
|
* 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();
|
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)
|
public List<Score> groupBy(DateUtils.TruncateField field)
|
||||||
{
|
{
|
||||||
computeAll();
|
computeAll();
|
||||||
|
|||||||
@@ -92,6 +92,26 @@ public class MemoryRepetitionList extends RepetitionList
|
|||||||
return oldestRep;
|
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
|
@Override
|
||||||
public void remove(@NonNull Repetition repetition)
|
public void remove(@NonNull Repetition repetition)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ public class MemoryScoreList extends ScoreList
|
|||||||
(s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp()));
|
(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
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Score getComputedByTimestamp(long timestamp)
|
public Score getComputedByTimestamp(long timestamp)
|
||||||
|
|||||||
@@ -125,6 +125,24 @@ public class SQLiteRepetitionList extends RepetitionList
|
|||||||
return record.toRepetition();
|
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
|
@Override
|
||||||
public void remove(@NonNull Repetition repetition)
|
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
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public Score getComputedByTimestamp(long timestamp)
|
public Score getComputedByTimestamp(long timestamp)
|
||||||
@@ -127,11 +150,7 @@ public class SQLiteScoreList extends ScoreList
|
|||||||
List<ScoreRecord> records = sqlite.query(query, params);
|
List<ScoreRecord> records = sqlite.query(query, params);
|
||||||
for (ScoreRecord record : records) record.habit = habitRecord;
|
for (ScoreRecord record : records) record.habit = habitRecord;
|
||||||
|
|
||||||
List<Score> scores = new LinkedList<>();
|
return toScores(records);
|
||||||
for (ScoreRecord rec : records)
|
|
||||||
scores.add(rec.toScore());
|
|
||||||
|
|
||||||
return scores;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -177,4 +196,12 @@ public class SQLiteScoreList extends ScoreList
|
|||||||
record.habit = habitRecord;
|
record.habit = habitRecord;
|
||||||
return record.toScore();
|
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
|
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));
|
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)
|
private void toggleRepetitions(final int from, final int to)
|
||||||
{
|
{
|
||||||
RepetitionList reps = habit.getRepetitions();
|
RepetitionList reps = habit.getRepetitions();
|
||||||
|
|||||||
@@ -134,4 +134,14 @@ public class DateUtilsTest extends BaseUnitTest
|
|||||||
assertThat(DateUtils.truncate(field, t1), equalTo(expected));
|
assertThat(DateUtils.truncate(field, t1), equalTo(expected));
|
||||||
assertThat(DateUtils.truncate(field, t2), 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