mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-07 01:28:52 -06:00
Allow user to skip days without breaking streak
Co-authored-by: Alinson S. Xavier <git@axavier.org>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user