From 1ef85c5b9b2766b1e609216278c8947a11649a1f Mon Sep 17 00:00:00 2001
From: Marten Gajda <marten@dmfs.org>
Date: Wed, 13 Dec 2017 22:40:55 +0100
Subject: [PATCH] Make sure non-recurring tasks get an INSTANCE_ORIGINAL_TIME
 of 0. Implements #577

This resulted in a minor SRP-ish refactoring to separate steps of instance value creation.
---
 opentasks-provider/build.gradle               |   2 +
 .../dmfs/provider/tasks/TaskProviderTest.java |  14 +-
 .../tasks/processors/tasks/Instantiating.java |  18 +-
 .../processors/tasks/instancedata/Dated.java  |  51 +++++
 .../tasks/instancedata/DueDated.java}         |  18 +-
 .../tasks/instancedata/Enduring.java          |  57 ++++++
 .../tasks/instancedata/Overridden.java        |  62 ++++++
 .../tasks/instancedata/StartDated.java        |  39 ++++
 .../tasks/instancedata/TaskRelated.java}      |   6 +-
 .../instancedata/VanillaInstanceData.java     |  45 +++++
 .../tasks/utils/InstanceDateTimeData.java     | 111 -----------
 .../tasks/utils/InstanceValuesIterable.java   |  37 ++--
 .../org/dmfs/provider/tasks/utils/Range.java  |   3 +
 .../org/dmfs/provider/tasks/utils/Zipped.java |  43 ++++
 .../tasks/instancedata/DatedTest.java         |  62 ++++++
 .../tasks/instancedata/DueDatedTest.java      |  84 ++++++++
 .../tasks/instancedata/EnduringTest.java      |  80 ++++++++
 .../tasks/instancedata/OverriddenTest.java    | 110 +++++++++++
 .../tasks/instancedata/StartDatedTest.java    |  84 ++++++++
 .../tasks/instancedata/TaskRelatedTest.java   |  44 +++++
 .../instancedata/VanillaInstanceDataTest.java |  52 +++++
 .../tasks/utils/ContentValuesWithLong.java    |  57 ++++++
 .../tasks/utils/InstanceDateTimeDataTest.java | 184 ------------------
 .../dmfs/provider/tasks/utils/ZippedTest.java |  56 ++++++
 24 files changed, 973 insertions(+), 346 deletions(-)
 create mode 100644 opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java
 rename opentasks-provider/src/main/java/org/dmfs/provider/tasks/{utils/SingleValueFunction.java => processors/tasks/instancedata/DueDated.java} (52%)
 create mode 100644 opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java
 create mode 100644 opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java
 create mode 100644 opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java
 rename opentasks-provider/src/main/java/org/dmfs/provider/tasks/{utils/WithTaskId.java => processors/tasks/instancedata/TaskRelated.java} (86%)
 create mode 100644 opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java
 delete mode 100644 opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceDateTimeData.java
 create mode 100644 opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java
 delete mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/InstanceDateTimeDataTest.java
 create mode 100644 opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java

diff --git a/opentasks-provider/build.gradle b/opentasks-provider/build.gradle
index 5fa66ba28..c96e40055 100644
--- a/opentasks-provider/build.gradle
+++ b/opentasks-provider/build.gradle
@@ -43,5 +43,7 @@ dependencies {
     androidTestImplementation 'com.android.support.test:rules:0.5'
     testImplementation 'org.robolectric:robolectric:' + ROBOLECTRIC_VERSION
     testImplementation 'junit:junit:4.12'
+    testImplementation 'org.mockito:mockito-core:2.10.0'
+    testImplementation 'org.dmfs:jems-testing:' + JEMS_VERSION
     testImplementation 'org.hamcrest:hamcrest-all:1.3'
 }
diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java
index 3c007d179..c30f40033 100644
--- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java
+++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java
@@ -234,7 +234,7 @@ public void testInsertTaskWithStartAndDue()
                 new Assert<>(task, new TimeData(start, due)),
                 new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task, new AllOf(
                         new EqArg(Instances.INSTANCE_START, start.getTimestamp()),
-                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()),
+                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, 0),
                         new EqArg(Instances.INSTANCE_DUE, due.getTimestamp()),
                         new EqArg(Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()),
                         new EqArg(Instances.INSTANCE_DUE_SORTING, due.shiftTimeZone(TimeZone.getDefault()).getInstance()),
@@ -269,7 +269,7 @@ public void testInsertTaskWithStartAndDueMovedForward()
                 new Assert<>(task, new TimeData(startNew, dueNew)),
                 new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task, new AllOf(
                         new EqArg(Instances.INSTANCE_START, startNew.getTimestamp()),
-                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, startNew.getTimestamp()),
+                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, 0),
                         new EqArg(Instances.INSTANCE_DUE, dueNew.getTimestamp()),
                         new EqArg(Instances.INSTANCE_START_SORTING, startNew.shiftTimeZone(TimeZone.getDefault()).getInstance()),
                         new EqArg(Instances.INSTANCE_DUE_SORTING, dueNew.shiftTimeZone(TimeZone.getDefault()).getInstance()),
@@ -304,7 +304,7 @@ public void testInsertTaskWithStartAndDueMovedBackwards()
                 new Assert<>(task, new TimeData(startNew, dueNew)),
                 new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task, new AllOf(
                         new EqArg(Instances.INSTANCE_START, startNew.getTimestamp()),
-                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, startNew.getTimestamp()),
+                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, 0),
                         new EqArg(Instances.INSTANCE_DUE, dueNew.getTimestamp()),
                         new EqArg(Instances.INSTANCE_START_SORTING, startNew.shiftTimeZone(TimeZone.getDefault()).getInstance()),
                         new EqArg(Instances.INSTANCE_DUE_SORTING, dueNew.shiftTimeZone(TimeZone.getDefault()).getInstance()),
@@ -335,7 +335,7 @@ public void testInsertTaskWithStartAndDueAddedAfterwards()
                 new Assert<>(task, new TimeData(start, due)),
                 new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task, new AllOf(
                         new EqArg(Instances.INSTANCE_START, start.getTimestamp()),
-                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()),
+                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, 0),
                         new EqArg(Instances.INSTANCE_DUE, due.getTimestamp()),
                         new EqArg(Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()),
                         new EqArg(Instances.INSTANCE_DUE_SORTING, due.shiftTimeZone(TimeZone.getDefault()).getInstance()),
@@ -371,7 +371,7 @@ public void testInsertWithStartAndDuration()
                         new EqArg(Instances.INSTANCE_DURATION, durationMillis),
                         new EqArg(Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()),
                         new EqArg(Instances.INSTANCE_DUE_SORTING, start.addDuration(duration).shiftTimeZone(TimeZone.getDefault()).getInstance()),
-                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()),
+                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, 0),
                         new EqArg(Tasks.TZ, "UTC")
                 ))
         ));
@@ -407,7 +407,7 @@ public void testInsertWithStartAndDurationChangeTimeZone()
                         new EqArg(Instances.INSTANCE_DURATION, durationMillis),
                         new EqArg(Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()),
                         new EqArg(Instances.INSTANCE_DUE_SORTING, start.addDuration(duration).shiftTimeZone(TimeZone.getDefault()).getInstance()),
-                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()),
+                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, 0),
                         new EqArg(Tasks.TZ, "America/New_York")
                 ))
         ));
@@ -447,7 +447,7 @@ public void testUpdateDue() throws Exception
                         new EqArg(Instances.INSTANCE_DURATION, due2.getTimestamp() - start.getTimestamp()),
                         new EqArg(Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()),
                         new EqArg(Instances.INSTANCE_DUE_SORTING, due2.shiftTimeZone(TimeZone.getDefault()).getInstance()),
-                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()),
+                        new EqArg(Instances.INSTANCE_ORIGINAL_TIME, 0),
                         new EqArg(Tasks.TZ, "UTC")
                 ))
         ));
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java
index 19d568bd2..059ada774 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java
@@ -20,7 +20,6 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 
-import org.dmfs.jems.function.BiFunction;
 import org.dmfs.jems.iterable.composite.Diff;
 import org.dmfs.jems.iterable.decorators.Mapped;
 import org.dmfs.jems.pair.Pair;
@@ -33,8 +32,7 @@
 import org.dmfs.provider.tasks.utils.InstanceValuesIterable;
 import org.dmfs.provider.tasks.utils.Limited;
 import org.dmfs.provider.tasks.utils.Range;
-import org.dmfs.provider.tasks.utils.SingleValueFunction;
-import org.dmfs.provider.tasks.utils.WithTaskId;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.TaskRelated;
 import org.dmfs.tasks.contract.TaskContract;
 
 import java.util.Locale;
@@ -136,7 +134,7 @@ private void createInstances(SQLiteDatabase db, TaskAdapter taskAdapter, long id
         // TODO: only limit future instances
         for (Single<ContentValues> values : new Limited<>(INSTANCE_COUNT_LIMIT, new InstanceValuesIterable(taskAdapter)))
         {
-            db.insert(TaskDatabaseHelper.Tables.INSTANCES, "", new WithTaskId(id, values).value());
+            db.insert(TaskDatabaseHelper.Tables.INSTANCES, "", new TaskRelated(id, values).value());
         }
     }
 
@@ -181,16 +179,12 @@ private void updateInstances(SQLiteDatabase db, TaskAdapter taskAdapter, long id
             // TODO: once we actually support recurrence we should only count future instances
 
             Iterable<Pair<Optional<ContentValues>, Optional<Integer>>> diff = new Diff<>(
-                    new Mapped<>(new SingleValueFunction<ContentValues>(), new Limited<>(INSTANCE_COUNT_LIMIT, new InstanceValuesIterable(taskAdapter))),
+                    new Mapped<>(Single::value, new Limited<>(INSTANCE_COUNT_LIMIT, new InstanceValuesIterable(taskAdapter))),
                     new Range(existingInstances.getCount()),
-                    new BiFunction<ContentValues, Integer, Integer>()
+                    (newInstanceValues, cursorRow) ->
                     {
-                        @Override
-                        public Integer value(ContentValues newInstanceValues, Integer cursorRow)
-                        {
-                            existingInstances.moveToPosition(cursorRow);
-                            return (int) (existingInstances.getLong(startIdx) - newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME));
-                        }
+                        existingInstances.moveToPosition(cursorRow);
+                        return (int) (existingInstances.getLong(startIdx) - newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME));
                     });
 
             // sync the instances table with the new instances
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java
new file mode 100644
index 000000000..3989e3d80
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.jems.single.Single;
+import org.dmfs.jems.single.decorators.DelegatingSingle;
+import org.dmfs.optional.Optional;
+import org.dmfs.provider.tasks.utils.Zipped;
+import org.dmfs.rfc5545.DateTime;
+
+import java.util.TimeZone;
+
+
+/**
+ * A {@link Single} of date and time {@link ContentValues} of an instance.
+ *
+ * @author Marten Gajda
+ */
+public final class Dated extends DelegatingSingle<ContentValues>
+{
+
+    public Dated(Optional<DateTime> dateTime, String timeStampColumn, String sortingColumn, Single<ContentValues> delegate)
+    {
+        super(new Zipped<>(
+                dateTime,
+                delegate,
+                (dateTime1, values) ->
+                {
+                    // add timestamp and sorting
+                    values.put(timeStampColumn, dateTime1.getTimestamp());
+                    values.put(sortingColumn, dateTime1.isAllDay() ? dateTime1.getInstance() : dateTime1.shiftTimeZone(TimeZone.getDefault()).getInstance());
+                    return values;
+                }));
+    }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/SingleValueFunction.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java
similarity index 52%
rename from opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/SingleValueFunction.java
rename to opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java
index 37ec7b9bd..5715d6a01 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/SingleValueFunction.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java
@@ -14,22 +14,26 @@
  * limitations under the License.
  */
 
-package org.dmfs.provider.tasks.utils;
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
 
-import org.dmfs.jems.function.Function;
 import org.dmfs.jems.single.Single;
+import org.dmfs.jems.single.decorators.DelegatingSingle;
+import org.dmfs.optional.Optional;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.tasks.contract.TaskContract;
 
 
 /**
- * A {@link Function} returning the value of a {@link Single}.
+ * A decorator to a {@link Single} of {@link ContentValues} adding due data.
  *
  * @author Marten Gajda
  */
-public final class SingleValueFunction<T> implements Function<Single<T>, T>
+public final class DueDated extends DelegatingSingle<ContentValues>
 {
-    @Override
-    public T value(Single<T> single)
+    public DueDated(Optional<DateTime> due, Single<ContentValues> delegate)
     {
-        return single.value();
+        super(new Dated(due, TaskContract.Instances.INSTANCE_DUE, TaskContract.Instances.INSTANCE_DUE_SORTING, delegate));
     }
 }
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java
new file mode 100644
index 000000000..9c1b1b575
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.jems.single.Single;
+import org.dmfs.optional.NullSafe;
+import org.dmfs.optional.composite.Zipped;
+import org.dmfs.tasks.contract.TaskContract;
+
+
+/**
+ * A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link TaskContract.Instances#INSTANCE_DURATION} field based on the
+ * already populated {@link TaskContract.Instances#INSTANCE_START} and {@link TaskContract.Instances#INSTANCE_DUE} fields.
+ *
+ * @author Marten Gajda
+ */
+public final class Enduring implements Single<ContentValues>
+{
+    private final Single<ContentValues> mDelegate;
+
+
+    public Enduring(Single<ContentValues> delegate)
+    {
+        mDelegate = delegate;
+    }
+
+
+    @Override
+    public ContentValues value()
+    {
+        ContentValues values = mDelegate.value();
+        // just store the difference between due and start, if both are present, otherwise store null
+        values.put(TaskContract.Instances.INSTANCE_DURATION,
+                new Zipped<>(
+                        new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_START)),
+                        new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_DUE)),
+                        (start, due) -> due - start)
+                        .value(null));
+        return values;
+    }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java
new file mode 100644
index 000000000..f4b8cd407
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.iterables.elementary.Seq;
+import org.dmfs.jems.optional.decorators.Mapped;
+import org.dmfs.jems.single.Single;
+import org.dmfs.optional.NullSafe;
+import org.dmfs.optional.Optional;
+import org.dmfs.optional.adapters.FirstPresent;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.tasks.contract.TaskContract;
+
+
+/**
+ * A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link TaskContract.Instances#INSTANCE_ORIGINAL_TIME} field based on
+ * the given {@link Optional} original start and the already populated {@link TaskContract.Instances#INSTANCE_START} and {@link
+ * TaskContract.Instances#INSTANCE_DUE} fields.
+ *
+ * @author Marten Gajda
+ */
+public final class Overridden implements Single<ContentValues>
+{
+    private final Optional<DateTime> mOriginalTime;
+    private final Single<ContentValues> mDelegate;
+
+
+    public Overridden(Optional<DateTime> originalTime, Single<ContentValues> delegate)
+    {
+        mOriginalTime = originalTime;
+        mDelegate = delegate;
+    }
+
+
+    @Override
+    public ContentValues value()
+    {
+        ContentValues values = mDelegate.value();
+        values.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, new FirstPresent<>(new Seq<>(
+                new Mapped<>(DateTime::getTimestamp, mOriginalTime),
+                new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_START)),
+                new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_DUE))))
+                .value(null));
+        return values;
+    }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java
new file mode 100644
index 000000000..29bfeb99c
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.jems.single.Single;
+import org.dmfs.jems.single.decorators.DelegatingSingle;
+import org.dmfs.optional.Optional;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.tasks.contract.TaskContract;
+
+
+/**
+ * A decorator to a {@link Single} of {@link ContentValues} adding start data.
+ *
+ * @author Marten Gajda
+ */
+public final class StartDated extends DelegatingSingle<ContentValues>
+{
+    public StartDated(Optional<DateTime> start, Single<ContentValues> delegate)
+    {
+        super(new Dated(start, TaskContract.Instances.INSTANCE_START, TaskContract.Instances.INSTANCE_START_SORTING, delegate));
+    }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/WithTaskId.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java
similarity index 86%
rename from opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/WithTaskId.java
rename to opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java
index 114946395..6d7501cc5 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/WithTaskId.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.dmfs.provider.tasks.utils;
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
 
 import android.content.ContentValues;
 
@@ -27,13 +27,13 @@
  *
  * @author Marten Gajda
  */
-public final class WithTaskId implements Single<ContentValues>
+public final class TaskRelated implements Single<ContentValues>
 {
     private final long mTaskId;
     private final Single<ContentValues> mDelegate;
 
 
-    public WithTaskId(long taskId, Single<ContentValues> delegate)
+    public TaskRelated(long taskId, Single<ContentValues> delegate)
     {
         mTaskId = taskId;
         mDelegate = delegate;
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java
new file mode 100644
index 000000000..d3ac32643
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.jems.single.Single;
+import org.dmfs.tasks.contract.TaskContract;
+
+
+/**
+ * A {@link Single} of instance data {@link ContentValues}. It initializes most columns with {@code null} values, except for {@link
+ * TaskContract.Instances#TASK_ID} which is left out and {@link TaskContract.Instances#INSTANCE_ORIGINAL_TIME} which is initialized with {@code 0}.
+ *
+ * @author Marten Gajda
+ */
+public final class VanillaInstanceData implements Single<ContentValues>
+{
+    @Override
+    public ContentValues value()
+    {
+        ContentValues values = new ContentValues(6);
+        values.putNull(TaskContract.Instances.INSTANCE_START);
+        values.putNull(TaskContract.Instances.INSTANCE_START_SORTING);
+        values.putNull(TaskContract.Instances.INSTANCE_DUE);
+        values.putNull(TaskContract.Instances.INSTANCE_DUE_SORTING);
+        values.putNull(TaskContract.Instances.INSTANCE_DURATION);
+        values.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 0);
+        return values;
+    }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceDateTimeData.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceDateTimeData.java
deleted file mode 100644
index e0cb4e89e..000000000
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceDateTimeData.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright 2017 dmfs GmbH
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.dmfs.provider.tasks.utils;
-
-import android.content.ContentValues;
-
-import org.dmfs.iterables.elementary.Seq;
-import org.dmfs.jems.function.BiFunction;
-import org.dmfs.jems.single.Single;
-import org.dmfs.optional.Optional;
-import org.dmfs.optional.adapters.FirstPresent;
-import org.dmfs.optional.composite.Zipped;
-import org.dmfs.rfc5545.DateTime;
-import org.dmfs.rfc5545.Duration;
-import org.dmfs.tasks.contract.TaskContract;
-
-import java.util.TimeZone;
-
-
-/**
- * A {@link Single} holding the date and time {@link ContentValues} of an instance.
- *
- * @author Marten Gajda
- */
-public final class InstanceDateTimeData implements Single<ContentValues>
-{
-    private final Optional<DateTime> mOptionalStart;
-    private final Optional<DateTime> mOptionalDue;
-    private final Optional<Duration> mOptionalDuration;
-    private final Optional<DateTime> mOriginalStart;
-
-
-    public InstanceDateTimeData(Optional<DateTime> optionalStart, Optional<DateTime> optionalDue, Optional<Duration> optionalDuration, Optional<DateTime> originalStart)
-    {
-        mOptionalStart = optionalStart;
-        mOptionalDue = optionalDue;
-        mOptionalDuration = optionalDuration;
-        mOriginalStart = originalStart;
-    }
-
-
-    @Override
-    public ContentValues value()
-    {
-        ContentValues instanceValues = new ContentValues();
-        TimeZone localTz = TimeZone.getDefault();
-
-        Optional<DateTime> effectiveDue = new FirstPresent<>(
-                new Seq<>(mOptionalDue, new Zipped<>(mOptionalStart, mOptionalDuration, new AddDurationBiFunction())));
-        Optional<Long> effectiveDuration = new Zipped<>(mOptionalStart, effectiveDue, new DurationBiFunction());
-
-        putDateValue(instanceValues, mOptionalStart, TaskContract.Instances.INSTANCE_START, TaskContract.Instances.INSTANCE_START_SORTING, localTz);
-        putDateValue(instanceValues, effectiveDue, TaskContract.Instances.INSTANCE_DUE, TaskContract.Instances.INSTANCE_DUE_SORTING, localTz);
-        instanceValues.put(TaskContract.Instances.INSTANCE_DURATION, effectiveDuration.value(null));
-        instanceValues.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME,
-                new FirstPresent<>(new Seq<>(mOriginalStart, mOptionalStart, effectiveDue)).value(new DateTime(0)).getTimestamp());
-
-        return instanceValues;
-    }
-
-
-    private void putDateValue(ContentValues values, Optional<DateTime> value, String timestampColumn, String sortingColumn, TimeZone localTimeZone)
-    {
-        if (value.isPresent())
-        {
-            // add timestamp and sorting
-            DateTime dateTime = value.value();
-            values.put(timestampColumn, dateTime.getTimestamp());
-            values.put(sortingColumn, dateTime.isAllDay() ? dateTime.getInstance() : dateTime.shiftTimeZone(localTimeZone).getInstance());
-        }
-        else
-        {
-            values.putNull(timestampColumn);
-            values.putNull(sortingColumn);
-        }
-    }
-
-
-    private static final class AddDurationBiFunction implements BiFunction<DateTime, Duration, DateTime>
-    {
-        @Override
-        public DateTime value(DateTime dateTime, Duration duration)
-        {
-            return dateTime.addDuration(duration);
-        }
-    }
-
-
-    private static final class DurationBiFunction implements BiFunction<DateTime, DateTime, Long>
-    {
-        @Override
-        public Long value(DateTime startDateTime, DateTime dueDateTime)
-        {
-            return dueDateTime.getTimestamp() - startDateTime.getTimestamp();
-        }
-    }
-}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java
index 9a9bc3096..3c884b6b3 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java
@@ -20,15 +20,18 @@
 
 import org.dmfs.iterables.elementary.Seq;
 import org.dmfs.iterators.SingletonIterator;
-import org.dmfs.jems.function.BiFunction;
 import org.dmfs.jems.single.Single;
 import org.dmfs.optional.NullSafe;
 import org.dmfs.optional.Optional;
 import org.dmfs.optional.adapters.FirstPresent;
 import org.dmfs.optional.composite.Zipped;
 import org.dmfs.provider.tasks.model.TaskAdapter;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.DueDated;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.Enduring;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.Overridden;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.StartDated;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.VanillaInstanceData;
 import org.dmfs.rfc5545.DateTime;
-import org.dmfs.rfc5545.Duration;
 
 import java.util.Iterator;
 
@@ -53,28 +56,18 @@ public InstanceValuesIterable(TaskAdapter taskAdapter)
     public Iterator<Single<ContentValues>> iterator()
     {
         Optional<DateTime> start = new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART));
-        Optional<DateTime> due = new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE));
-
-        Optional<Duration> effectiveDuration = new FirstPresent<>(
+        // effective due is either the actual due, start + duration or absent
+        Optional<DateTime> effectiveDue = new FirstPresent<>(
                 new Seq<>(
-                        new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)),
-                        new Zipped<>(start, due, new DateTimeDurationBiFunction())));
-
-        // TODO: implement support for recurrence, for now we only return the first instance
-        return new SingletonIterator<Single<ContentValues>>(
-                new InstanceDateTimeData(start, due, effectiveDuration, new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME))));
-    }
+                        new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE)),
+                        new Zipped<>(start, new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration)));
 
+        Single<ContentValues> baseData = new Enduring(new DueDated(effectiveDue, new StartDated(start, new VanillaInstanceData())));
 
-    /**
-     * A {@link BiFunction} returning the duration between two {@link DateTime} values.
-     */
-    private final static class DateTimeDurationBiFunction implements BiFunction<DateTime, DateTime, Duration>
-    {
-        @Override
-        public Duration value(DateTime start, DateTime due)
-        {
-            return new Duration(1, 0, (int) ((due.getTimestamp() - start.getTimestamp()) / 1000));
-        }
+        // TODO: implement support for recurrence, for now we only return the first instance
+        return new SingletonIterator<>(mTaskAdapter.isRecurring() ?
+                new Overridden(new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)), baseData)
+                :
+                baseData);
     }
 }
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java
index 52e144e5e..731e4243f 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java
@@ -23,9 +23,12 @@
 
 /**
  * An {@link Iterable} which iterates a range of numbers.
+ * <p>
+ * TODO: implement in jems
  *
  * @author Marten Gajda
  */
+@Deprecated
 public final class Range implements Iterable<Integer>
 {
     private final int mStart;
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java
new file mode 100644
index 000000000..06a5a3958
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.utils;
+
+import org.dmfs.jems.function.BiFunction;
+import org.dmfs.jems.optional.decorators.Mapped;
+import org.dmfs.jems.single.Single;
+import org.dmfs.jems.single.combined.Backed;
+import org.dmfs.jems.single.decorators.DelegatingSingle;
+import org.dmfs.optional.Optional;
+
+
+/**
+ * Experimental {@link Single} which applies a {@link BiFunction} based on the presence of an {@link Optional}.
+ * <p>
+ * TODO: maybe a more appropriate name?
+ * <p>
+ * TODO: move to jems
+ *
+ * @author Marten Gajda
+ */
+@Deprecated
+public final class Zipped<T> extends DelegatingSingle<T>
+{
+    public <V> Zipped(Optional<V> optionalValue, Single<T> delegate, BiFunction<V, T, T> function)
+    {
+        super(new Backed<T>(new Mapped<>(from -> function.value(from, delegate.value()), optionalValue), delegate));
+    }
+}
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java
new file mode 100644
index 000000000..5c8397a3d
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.optional.Present;
+import org.dmfs.provider.tasks.utils.ContentValuesWithLong;
+import org.dmfs.rfc5545.DateTime;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.dmfs.optional.Absent.absent;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class DatedTest
+{
+
+    @Test
+    public void testAbsent() throws Exception
+    {
+        ContentValues instanceData = new Dated(absent(), "ts", "sorting", ContentValues::new).value();
+        // this shouldn't really add any values and go by the "defaults"
+        assertThat(instanceData.size(), is(0));
+    }
+
+
+    @Test
+    public void testPresent() throws Exception
+    {
+        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
+
+        ContentValues instanceData = new Dated(new Present<>(start), "ts", "sorting", ContentValues::new).value();
+
+        assertThat(instanceData, new ContentValuesWithLong("ts", start.getTimestamp()));
+        assertThat(instanceData, new ContentValuesWithLong("sorting", start.getInstance()));
+        assertThat(instanceData.size(), is(2));
+    }
+}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java
new file mode 100644
index 000000000..0db5b633f
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.optional.Present;
+import org.dmfs.provider.tasks.utils.ContentValuesWithLong;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.tasks.contract.TaskContract;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.TimeZone;
+
+import static org.dmfs.optional.Absent.absent;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class DueDatedTest
+{
+
+    @Test
+    public void testNone() throws Exception
+    {
+        ContentValues instanceData = new DueDated(absent(), ContentValues::new).value();
+
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, nullValue(Long.class)));
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, nullValue(Long.class)));
+        // this doesn't actually add anything, the ContentValues are expected to contain null values.
+        assertThat(instanceData.size(), is(0));
+    }
+
+
+    @Test
+    public void testStartEurope() throws Exception
+    {
+        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
+
+        ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value();
+
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp()));
+        assertThat(instanceData,
+                new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()));
+        assertThat(instanceData.size(), is(2));
+    }
+
+
+    @Test
+    public void testStartAmerica() throws Exception
+    {
+        DateTime start = DateTime.parse("America/New_York", "20171208T125500");
+
+        ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value();
+
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp()));
+        assertThat(instanceData,
+                new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()));
+        assertThat(instanceData.size(), is(2));
+    }
+}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java
new file mode 100644
index 000000000..5c7b7e5eb
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.provider.tasks.utils.ContentValuesWithLong;
+import org.dmfs.tasks.contract.TaskContract;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class EnduringTest
+{
+    @Test
+    public void testNoValue() throws Exception
+    {
+        assertThat(new Enduring(ContentValues::new), hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class))));
+        assertThat(new Enduring(ContentValues::new).value().size(), is(1));
+    }
+
+
+    @Test
+    public void testStartValue() throws Exception
+    {
+        ContentValues values = new ContentValues(1);
+        values.put(TaskContract.Instances.INSTANCE_START, 10);
+        assertThat(new Enduring(() -> new ContentValues(values)),
+                hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class))));
+        assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2));
+    }
+
+
+    @Test
+    public void testDueValue() throws Exception
+    {
+        ContentValues values = new ContentValues(1);
+        values.put(TaskContract.Instances.INSTANCE_DUE, 10);
+        assertThat(new Enduring(() -> new ContentValues(values)),
+                hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class))));
+        assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2));
+    }
+
+
+    @Test
+    public void testStartDueValue() throws Exception
+    {
+        ContentValues values = new ContentValues(2);
+        values.put(TaskContract.Instances.INSTANCE_START, 1);
+        values.put(TaskContract.Instances.INSTANCE_DUE, 10);
+        assertThat(new Enduring(() -> new ContentValues(values)), hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, 9)));
+        assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(3));
+    }
+}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java
new file mode 100644
index 000000000..3882bc523
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.optional.Present;
+import org.dmfs.provider.tasks.utils.ContentValuesWithLong;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.tasks.contract.TaskContract;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.dmfs.optional.Absent.absent;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class OverriddenTest
+{
+    @Test
+    public void testAbsent() throws Exception
+    {
+        ContentValues instanceData = new Overridden(absent(), ContentValues::new).value();
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class)));
+        assertThat(instanceData.size(), is(1));
+    }
+
+
+    @Test
+    public void testAbsentWithStart() throws Exception
+    {
+        ContentValues values = new ContentValues();
+        values.put(TaskContract.Instances.INSTANCE_START, 10);
+
+        ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value();
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 10));
+        assertThat(instanceData.size(), is(2));
+    }
+
+
+    @Test
+    public void testAbsentWithDue() throws Exception
+    {
+        ContentValues values = new ContentValues();
+        values.put(TaskContract.Instances.INSTANCE_DUE, 20);
+
+        ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value();
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 20));
+        assertThat(instanceData.size(), is(2));
+    }
+
+
+    @Test
+    public void testAbsentWithStartAndDue() throws Exception
+    {
+        ContentValues values = new ContentValues();
+        values.put(TaskContract.Instances.INSTANCE_START, 10);
+        values.put(TaskContract.Instances.INSTANCE_DUE, 20);
+
+        ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value();
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 10));
+        assertThat(instanceData.size(), is(3));
+    }
+
+
+    @Test
+    public void testPresent() throws Exception
+    {
+
+        ContentValues instanceData = new Overridden(new Present<>(new DateTime(40)), ContentValues::new).value();
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40));
+        assertThat(instanceData.size(), is(1));
+    }
+
+
+    @Test
+    public void testPresentWithStartAndDue() throws Exception
+    {
+        ContentValues values = new ContentValues();
+        values.put(TaskContract.Instances.INSTANCE_START, 10);
+        values.put(TaskContract.Instances.INSTANCE_DUE, 20);
+
+        ContentValues instanceData = new Overridden(new Present<>(new DateTime(40)), () -> new ContentValues(values)).value();
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40));
+        assertThat(instanceData.size(), is(3));
+    }
+}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java
new file mode 100644
index 000000000..991af0862
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.optional.Present;
+import org.dmfs.provider.tasks.utils.ContentValuesWithLong;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.tasks.contract.TaskContract;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.TimeZone;
+
+import static org.dmfs.optional.Absent.absent;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class StartDatedTest
+{
+
+    @Test
+    public void testNone() throws Exception
+    {
+        ContentValues instanceData = new StartDated(absent(), ContentValues::new).value();
+
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, nullValue(Long.class)));
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, nullValue(Long.class)));
+        // this doesn't actually add anything, the ContentValues are expected to contain null values.
+        assertThat(instanceData.size(), is(0));
+    }
+
+
+    @Test
+    public void testStartEurope() throws Exception
+    {
+        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
+
+        ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value();
+
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp()));
+        assertThat(instanceData,
+                new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()));
+        assertThat(instanceData.size(), is(2));
+    }
+
+
+    @Test
+    public void testStartAmerica() throws Exception
+    {
+        DateTime start = DateTime.parse("America/New_York", "20171208T125500");
+
+        ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value();
+
+        assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp()));
+        assertThat(instanceData,
+                new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance()));
+        assertThat(instanceData.size(), is(2));
+    }
+}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java
new file mode 100644
index 000000000..3f4ae122e
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.provider.tasks.utils.ContentValuesWithLong;
+import org.dmfs.tasks.contract.TaskContract;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class TaskRelatedTest
+{
+    @Test
+    public void testValue() throws Exception
+    {
+        assertThat(new TaskRelated(123, ContentValues::new), hasValue(new ContentValuesWithLong(TaskContract.Instances.TASK_ID, 123)));
+    }
+}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java
new file mode 100644
index 000000000..f3a5678fe
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks.instancedata;
+
+import android.content.ContentValues;
+
+import org.dmfs.tasks.contract.TaskContract;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class VanillaInstanceDataTest
+{
+    @Test
+    public void testValue() throws Exception
+    {
+        ContentValues values = new VanillaInstanceData().value();
+        assertThat(values.get(TaskContract.Instances.INSTANCE_START), nullValue());
+        assertThat(values.get(TaskContract.Instances.INSTANCE_START_SORTING), nullValue());
+        assertThat(values.get(TaskContract.Instances.INSTANCE_DUE), nullValue());
+        assertThat(values.get(TaskContract.Instances.INSTANCE_DUE_SORTING), nullValue());
+        assertThat(values.get(TaskContract.Instances.INSTANCE_DURATION), nullValue());
+        assertThat(values.get(TaskContract.Instances.INSTANCE_ORIGINAL_TIME), is(0));
+        assertThat(values.size(), is(6));
+    }
+
+}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java
new file mode 100644
index 000000000..cf03faded
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.utils;
+
+import android.content.ContentValues;
+
+import org.hamcrest.FeatureMatcher;
+import org.hamcrest.Matcher;
+
+import static org.hamcrest.Matchers.is;
+
+
+/**
+ * A {@link Matcher} to test if {@link ContentValues} contain a specific Long value.
+ * <p>
+ * TODO: can we convert that into a more generic {@link ContentValues} matcher? It might be useful in other places.
+ *
+ * TODO: also consider moving this to "Test-Bolts"
+ */
+public final class ContentValuesWithLong extends FeatureMatcher<ContentValues, Long>
+{
+    private final String mKey;
+
+
+    public ContentValuesWithLong(String valueKey, long value)
+    {
+        this(valueKey, is(value));
+    }
+
+
+    public ContentValuesWithLong(String valueKey, Matcher<Long> matcher)
+    {
+        super(matcher, "Long value " + valueKey, "Long value " + valueKey);
+        mKey = valueKey;
+    }
+
+
+    @Override
+    protected Long featureValueOf(ContentValues actual)
+    {
+        return actual.getAsLong(mKey);
+    }
+}
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/InstanceDateTimeDataTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/InstanceDateTimeDataTest.java
deleted file mode 100644
index 934d341e5..000000000
--- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/InstanceDateTimeDataTest.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright 2017 dmfs GmbH
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.dmfs.provider.tasks.utils;
-
-import android.content.ContentValues;
-
-import org.dmfs.optional.Absent;
-import org.dmfs.optional.Present;
-import org.dmfs.rfc5545.DateTime;
-import org.dmfs.rfc5545.Duration;
-import org.dmfs.tasks.contract.TaskContract;
-import org.hamcrest.FeatureMatcher;
-import org.hamcrest.Matcher;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.nullValue;
-import static org.junit.Assert.assertThat;
-
-
-/**
- * @author Marten Gajda
- */
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = Config.NONE)
-public class InstanceDateTimeDataTest
-{
-
-    @Test
-    public void testNone() throws Exception
-    {
-        ContentValues instanceData = new InstanceDateTimeData(Absent.<DateTime>absent(), Absent.<DateTime>absent(), Absent.<Duration>absent(),
-                Absent.<DateTime>absent()).value();
-
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START_SORTING, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE_SORTING, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 0));
-    }
-
-
-    @Test
-    public void testStart() throws Exception
-    {
-        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
-
-        ContentValues instanceData = new InstanceDateTimeData(new Present<>(start), Absent.<DateTime>absent(), Absent.<Duration>absent(),
-                new Present<>(start)).value();
-
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START, start.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START_SORTING, start.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE_SORTING, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()));
-    }
-
-
-    @Test
-    public void testStartDuration() throws Exception
-    {
-        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
-        DateTime due = DateTime.parse("Europe/Berlin", "20171208T155500");
-        Duration duration = Duration.parse("PT3H");
-
-        ContentValues instanceData = new InstanceDateTimeData(new Present<>(start), Absent.<DateTime>absent(), new Present<>(duration),
-                new Present<>(start)).value();
-
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START, start.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START_SORTING, start.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE, due.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE_SORTING, due.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DURATION, duration.toMillis()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()));
-    }
-
-
-    @Test
-    public void testStartDue() throws Exception
-    {
-        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
-        DateTime due = DateTime.parse("Europe/Berlin", "20171208T155500");
-        Duration duration = Duration.parse("PT3H");
-
-        ContentValues instanceData = new InstanceDateTimeData(new Present<>(start), new Present<>(due), Absent.<Duration>absent(),
-                new Present<>(start)).value();
-
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START, start.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START_SORTING, start.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE, due.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE_SORTING, due.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DURATION, duration.toMillis()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()));
-    }
-
-
-    @Test
-    public void testStartDueOriginal() throws Exception
-    {
-        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
-        DateTime due = DateTime.parse("Europe/Berlin", "20171208T155500");
-        Duration duration = Duration.parse("PT3H");
-        DateTime original = DateTime.parse("Europe/Berlin", "20171210T155500");
-
-        ContentValues instanceData = new InstanceDateTimeData(new Present<>(start), new Present<>(due), Absent.<Duration>absent(),
-                new Present<>(original)).value();
-
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START, start.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START_SORTING, start.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE, due.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE_SORTING, due.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DURATION, duration.toMillis()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, original.getTimestamp()));
-    }
-
-
-    @Test
-    public void testStartDueAbsentOriginal() throws Exception
-    {
-        DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500");
-        DateTime due = DateTime.parse("Europe/Berlin", "20171208T155500");
-        Duration duration = Duration.parse("PT3H");
-
-        ContentValues instanceData = new InstanceDateTimeData(new Present<>(start), new Present<>(due), Absent.<Duration>absent(),
-                Absent.<DateTime>absent()).value();
-
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START, start.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_START_SORTING, start.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE, due.getTimestamp()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DUE_SORTING, due.getInstance()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_DURATION, duration.toMillis()));
-        assertThat(instanceData, new Contains(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()));
-    }
-
-
-    /**
-     * A {@link Matcher} to test if {@link ContentValues} contain a specific Long value.
-     * <p>
-     * TODO: can we convert that into a more generic {@link ContentValues} matcher? It might be useful in other places.
-     */
-    private final class Contains extends FeatureMatcher<ContentValues, Long>
-    {
-        private final String mKey;
-
-
-        public Contains(String valueKey, long value)
-        {
-            this(valueKey, is(value));
-        }
-
-
-        public Contains(String valueKey, Matcher<Long> matcher)
-        {
-            super(matcher, "Long value " + valueKey, "Long value " + valueKey);
-            mKey = valueKey;
-        }
-
-
-        @Override
-        protected Long featureValueOf(ContentValues actual)
-        {
-            return actual.getAsLong(mKey);
-        }
-    }
-}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java
new file mode 100644
index 000000000..1ee7ac873
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.utils;
+
+import org.dmfs.jems.function.BiFunction;
+import org.dmfs.jems.single.elementary.ValueSingle;
+import org.dmfs.optional.Present;
+import org.junit.Test;
+
+import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue;
+import static org.dmfs.jems.mockito.doubles.TestDoubles.dummy;
+import static org.dmfs.jems.mockito.doubles.TestDoubles.failingMock;
+import static org.dmfs.optional.Absent.absent;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doReturn;
+
+
+/**
+ * @author Marten Gajda
+ */
+public class ZippedTest
+{
+    @Test
+    public void testPresent()
+    {
+        Object dummyPresentValue = new Object();
+        Object dummySingleValue = new Object();
+        Object dummyResult = new Object();
+        BiFunction<Object, Object, Object> mockFunction = failingMock(BiFunction.class);
+        doReturn(dummyResult).when(mockFunction).value(dummyPresentValue, dummySingleValue);
+        assertThat(new Zipped<>(new Present<>(dummyPresentValue), new ValueSingle<>(dummySingleValue), mockFunction), hasValue(sameInstance(dummyResult)));
+    }
+
+
+    @Test
+    public void testAbsent()
+    {
+        Object dummyObject = new Object();
+        assertThat(new Zipped<>(absent(), new ValueSingle<>(dummyObject), dummy(BiFunction.class)), hasValue(sameInstance(dummyObject)));
+    }
+}
\ No newline at end of file