Allow user to skip days without breaking streak

Co-authored-by: Alinson S. Xavier <git@axavier.org>
This commit is contained in:
KristianTashkov
2020-08-22 15:35:35 -05:00
committed by Alinson S. Xavier
parent d9ff429c28
commit 1a05f7d85d
31 changed files with 127 additions and 292 deletions

View File

@@ -72,10 +72,6 @@ public class CommandParser
.fromJson(json, EditHabitCommand.Record.class)
.toCommand(modelFactory, habitList);
if (event.equals("Toggle")) return gson
.fromJson(json, ToggleRepetitionCommand.Record.class)
.toCommand(habitList);
if (event.equals("Unarchive")) return gson
.fromJson(json, UnarchiveHabitsCommand.Record.class)
.toCommand(habitList);

View File

@@ -58,8 +58,11 @@ public class CreateRepetitionCommand extends Command
previousRep = reps.getByTimestamp(timestamp);
if (previousRep != null) reps.remove(previousRep);
newRep = new Repetition(timestamp, value);
reps.add(newRep);
if (value > 0)
{
newRep = new Repetition(timestamp, value);
reps.add(newRep);
}
habit.invalidateNewerThan(timestamp);
}
@@ -80,9 +83,7 @@ public class CreateRepetitionCommand extends Command
@Override
public void undo()
{
if(newRep == null) throw new IllegalStateException();
habit.getRepetitions().remove(newRep);
if(newRep != null) habit.getRepetitions().remove(newRep);
if (previousRep != null) habit.getRepetitions().add(previousRep);
habit.invalidateNewerThan(timestamp);
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.commands;
import androidx.annotation.*;
import org.isoron.uhabits.core.models.*;
/**
* Command to toggle a repetition.
*/
public class ToggleRepetitionCommand extends Command
{
@NonNull
private HabitList list;
final Timestamp timestamp;
@NonNull
final Habit habit;
public ToggleRepetitionCommand(@NonNull HabitList list,
@NonNull Habit habit,
Timestamp timestamp)
{
super();
this.list = list;
this.timestamp = timestamp;
this.habit = habit;
}
@Override
public void execute()
{
habit.getRepetitions().toggle(timestamp);
list.update(habit);
}
@NonNull
public Habit getHabit()
{
return habit;
}
@Override
@NonNull
public Record toRecord()
{
return new Record(this);
}
@Override
public void undo()
{
execute();
}
public static class Record
{
@NonNull
public String id;
@NonNull
public String event = "Toggle";
public long habit;
public long repTimestamp;
public Record(@NonNull ToggleRepetitionCommand command)
{
id = command.getId();
Long habitId = command.habit.getId();
if (habitId == null) throw new RuntimeException("Habit not saved");
this.repTimestamp = command.timestamp.getUnixTime();
this.habit = habitId;
}
public ToggleRepetitionCommand toCommand(@NonNull HabitList habitList)
{
Habit h = habitList.getById(habit);
if (h == null) throw new HabitNotFoundException();
ToggleRepetitionCommand command;
command = new ToggleRepetitionCommand(
habitList, h, new Timestamp(repTimestamp));
command.setId(id);
return command;
}
}
}

View File

@@ -37,6 +37,11 @@ import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
@ThreadSafe
public final class Checkmark
{
/**
* Indicates that there was an explicit skip at the timestamp.
*/
public static final int SKIPPED = 3;
/**
* Indicates that there was a repetition at the timestamp.
*/
@@ -59,8 +64,8 @@ public final class Checkmark
/**
* The value of the checkmark.
* <p>
* For boolean habits, this equals either UNCHECKED, CHECKED_EXPLICITLY,
* or CHECKED_IMPLICITLY.
* For boolean habits, this equals either UNCHECKED, CHECKED_EXPLICITLY, CHECKED_IMPLICITLY
* or SKIPPED.
* <p>
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is stored as 1500.

View File

@@ -79,7 +79,7 @@ public abstract class CheckmarkList
{
Timestamp date = rep.getTimestamp();
int offset = date.daysUntil(today);
checkmarks.set(offset, new Checkmark(date, CHECKED_EXPLICITLY));
checkmarks.set(offset, new Checkmark(date, rep.getValue()));
}
return checkmarks;

View File

@@ -27,10 +27,11 @@ import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import static org.isoron.uhabits.core.models.Checkmark.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* Represents a record that the user has performed a certain habit at a certain
* Represents a record that the user has performed or skipped a certain habit at a certain
* date.
*/
public final class Repetition
@@ -41,9 +42,9 @@ public final class Repetition
/**
* The value of the repetition.
*
* For boolean habits, this always equals Checkmark.CHECKED_EXPLICITLY.
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is here stored as 1500.
* For boolean habits, this equals CHECKED_EXPLICITLY if performed or SKIPPED if skipped.
* For numerical habits, this number is stored in thousandths. That is, if the user enters
* value 1.50 on the app, it is here stored as 1500.
*/
private final int value;
@@ -61,6 +62,21 @@ public final class Repetition
this.value = value;
}
public static int nextToggleValue(int value)
{
switch(value) {
case UNCHECKED:
case CHECKED_IMPLICITLY:
return CHECKED_EXPLICITLY;
case CHECKED_EXPLICITLY:
return SKIPPED;
default:
case SKIPPED:
return UNCHECKED;
}
}
@Override
public boolean equals(Object o)
{

View File

@@ -119,15 +119,15 @@ public abstract class RepetitionList
public abstract Repetition getNewest();
/**
* Returns the total number of repetitions for each month, from the first
* Returns the total number of successful repetitions for each month, from the first
* repetition until today, grouped by day of week.
* <p>
* The repetitions are returned in a HashMap. The key is the timestamp for
* the first day of the month, at midnight (00:00). The value is an integer
* array with 7 entries. The first entry contains the total number of
* repetitions during the specified month that occurred on a Saturday. The
* successful repetitions during the specified month that occurred on a Saturday. The
* second entry corresponds to Sunday, and so on. If there are no
* repetitions during a certain month, the value is null.
* successful repetitions during a certain month, the value is null.
*
* @return total number of repetitions by month versus day of week
*/
@@ -140,6 +140,9 @@ public abstract class RepetitionList
for (Repetition r : reps)
{
if (!habit.isNumerical() && r.getValue() != Checkmark.CHECKED_EXPLICITLY)
continue;
Calendar date = r.getTimestamp().toCalendar();
int weekday = r.getTimestamp().getWeekday();
date.set(Calendar.DAY_OF_MONTH, 1);
@@ -202,12 +205,6 @@ public abstract class RepetitionList
return rep;
}
/**
* Returns the number of all repetitions
*
* @return number of all repetitions
*/
@NonNull
public abstract long getTotalCount();
public void toggle(Timestamp timestamp, int value)

View File

@@ -275,17 +275,16 @@ public abstract class ScoreList implements Iterable<Score>
for (int i = 0; i < checkmarkValues.length; i++)
{
double value = checkmarkValues[checkmarkValues.length - i - 1];
if (habit.isNumerical())
if (!habit.isNumerical() || value != Checkmark.SKIPPED)
{
value /= 1000;
value /= habit.getTargetValue();
if (habit.isNumerical())
{
value /= 1000;
value /= habit.getTargetValue();
}
value = Math.min(1, value);
previousValue = Score.compute(freq, previousValue, value);
}
if (!habit.isNumerical() && value > 0) value = 1;
previousValue = Score.compute(freq, previousValue, value);
scores.add(new Score(from.plus(i), previousValue));
}

View File

@@ -121,7 +121,11 @@ public class MemoryRepetitionList extends RepetitionList
@Override
public long getTotalCount()
{
return list.size();
int count = 0;
for (Repetition rep : list)
if (rep.getValue() == Checkmark.CHECKED_EXPLICITLY)
count++;
return count;
}
@Override

View File

@@ -59,7 +59,7 @@ public class ReminderScheduler implements CommandRunner.Listener
public synchronized void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (command instanceof ToggleRepetitionCommand) return;
if (command instanceof CreateRepetitionCommand) return;
if (command instanceof ChangeHabitColorCommand) return;
scheduleAll();
}

View File

@@ -76,17 +76,11 @@ public class NotificationTray
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (command instanceof ToggleRepetitionCommand)
if (command instanceof CreateRepetitionCommand)
{
ToggleRepetitionCommand toggleCmd =
(ToggleRepetitionCommand) command;
Habit habit = toggleCmd.getHabit();
taskRunner.execute(() ->
{
if (habit.getCheckmarks().getTodayValue() !=
Checkmark.UNCHECKED) cancel(habit);
});
CreateRepetitionCommand createCmd = (CreateRepetitionCommand) command;
Habit habit = createCmd.getHabit();
cancel(habit);
}
if (command instanceof DeleteHabitsCommand)

View File

@@ -149,11 +149,11 @@ public class ListHabitsBehavior
if (prefs.isFirstRun()) onFirstRun();
}
public void onToggle(@NonNull Habit habit, Timestamp timestamp)
public void onToggle(@NonNull Habit habit, Timestamp timestamp, int value)
{
commandRunner.execute(
new ToggleRepetitionCommand(habitList, habit, timestamp),
habit.getId());
new CreateRepetitionCommand(habit, timestamp, value),
habit.getId());
}
public enum Message

View File

@@ -56,10 +56,10 @@ public class ShowHabitBehavior
screen.showEditHistoryScreen();
}
public void onToggleCheckmark(Timestamp timestamp)
public void onToggleCheckmark(Timestamp timestamp, int value)
{
commandRunner.execute(
new ToggleRepetitionCommand(habitList, habit, timestamp), null);
new CreateRepetitionCommand(habit, timestamp, value), null);
}
public interface Screen

View File

@@ -110,7 +110,7 @@ public class ShowHabitMenuBehavior
if (i % 7 == 0) strength = max(0, min(100, strength + 10 * random.nextGaussian()));
if (random.nextInt(100) > strength) continue;
int value = 1;
int value = Checkmark.CHECKED_EXPLICITLY;
if (habit.isNumerical())
value = (int) (1000 + 250 * random.nextGaussian() * strength / 100) * 1000;

View File

@@ -29,19 +29,15 @@ import javax.inject.*;
public class WidgetBehavior
{
private HabitList habitList;
@NonNull
private final CommandRunner commandRunner;
private NotificationTray notificationTray;
@Inject
public WidgetBehavior(@NonNull HabitList habitList,
@NonNull CommandRunner commandRunner,
public WidgetBehavior(@NonNull CommandRunner commandRunner,
@NonNull NotificationTray notificationTray)
{
this.habitList = habitList;
this.commandRunner = commandRunner;
this.notificationTray = notificationTray;
}
@@ -51,7 +47,7 @@ public class WidgetBehavior
notificationTray.cancel(habit);
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
if (rep != null) return;
performToggle(habit, timestamp);
performToggle(habit, timestamp, Checkmark.CHECKED_EXPLICITLY);
}
public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp)
@@ -59,18 +55,20 @@ public class WidgetBehavior
notificationTray.cancel(habit);
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
if (rep == null) return;
performToggle(habit, timestamp);
performToggle(habit, timestamp, Checkmark.UNCHECKED);
}
public void onToggleRepetition(@NonNull Habit habit, Timestamp timestamp)
{
performToggle(habit, timestamp);
Repetition previous = habit.getRepetitions().getByTimestamp(timestamp);
if(previous == null) performToggle(habit, timestamp, Checkmark.CHECKED_EXPLICITLY);
else performToggle(habit, timestamp, Repetition.nextToggleValue(previous.getValue()));
}
private void performToggle(@NonNull Habit habit, Timestamp timestamp)
private void performToggle(@NonNull Habit habit, Timestamp timestamp, int value)
{
commandRunner.execute(
new ToggleRepetitionCommand(habitList, habit, timestamp),
new CreateRepetitionCommand(habit, timestamp, value),
habit.getId());
}

View File

@@ -136,20 +136,6 @@ public class CommandParserTest extends BaseUnitTest
.getData()));
}
@Test
public void testDecodeToggleCommand() throws JSONException
{
ToggleRepetitionCommand original, decoded;
original = new ToggleRepetitionCommand(habitList, habit,
Timestamp.ZERO.plus(100));
decoded = (ToggleRepetitionCommand) parser.parse(original.toJson());
MatcherAssert.assertThat(decoded.getId(), equalTo(original.getId()));
MatcherAssert.assertThat(decoded.timestamp, equalTo(original
.timestamp));
MatcherAssert.assertThat(decoded.habit, equalTo(original.habit));
}
@Test
public void testDecodeUnarchiveCommand() throws JSONException
{

View File

@@ -1,76 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.commands;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.junit.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
public class ToggleRepetitionCommandTest extends BaseUnitTest
{
private ToggleRepetitionCommand command;
private Habit habit;
private Timestamp today;
@Override
@Before
public void setUp() throws Exception
{
super.setUp();
habit = fixtures.createShortHabit();
habitList.add(habit);
today = DateUtils.getToday();
command = new ToggleRepetitionCommand(habitList, habit, today);
}
@Test
public void testExecuteUndoRedo()
{
assertTrue(habit.getRepetitions().containsTimestamp(today));
command.execute();
assertFalse(habit.getRepetitions().containsTimestamp(today));
command.undo();
assertTrue(habit.getRepetitions().containsTimestamp(today));
command.execute();
assertFalse(habit.getRepetitions().containsTimestamp(today));
}
@Test
public void testRecord()
{
ToggleRepetitionCommand.Record rec = command.toRecord();
ToggleRepetitionCommand other = rec.toCommand(habitList);
assertThat(command.getId(), equalTo(other.getId()));
assertThat(command.timestamp, equalTo(other.timestamp));
assertThat(command.habit, equalTo(other.habit));
}
}

View File

@@ -84,9 +84,7 @@ public class HabitCardListCacheTest extends BaseUnitTest
{
Habit h2 = habitList.getByPosition(2);
Timestamp today = DateUtils.getToday();
commandRunner.execute(new ToggleRepetitionCommand(habitList, h2, today),
h2.getId());
commandRunner.execute(new CreateRepetitionCommand(h2, today, Checkmark.UNCHECKED), h2.getId());
verify(listener).onItemChanged(2);
verify(listener).onRefreshFinished();
verifyNoMoreInteractions(listener);

View File

@@ -173,7 +173,7 @@ public class ListHabitsBehaviorTest extends BaseUnitTest
public void testOnToggle()
{
assertTrue(habit1.isCompletedToday());
behavior.onToggle(habit1, DateUtils.getToday());
behavior.onToggle(habit1, DateUtils.getToday(), Checkmark.UNCHECKED);
assertFalse(habit1.isCompletedToday());
}