test: dates with precision

test: selectInJson
refactor: remove unused selectMultipleInJson and date relative date formatting
main
daniel-michel 2024-01-09 21:02:18 +01:00
parent 0a9a8d033f
commit 57708bc894
15 changed files with 265 additions and 230 deletions

View File

@ -12,15 +12,6 @@ Iterable<T> selectInJson<T>(dynamic json, String path) {
return selectInJsonWithPath<T>(json, path).map((e) => e.value);
}
Map<String, Iterable<dynamic>> selectMultipleInJson(
dynamic json, Map<String, String> selector) {
Map<String, Iterable<dynamic>> result = {};
for (String key in selector.keys) {
result[key] = selectInJsonWithPath(json, selector[key]!);
}
return result;
}
/// Select values in nested [List] and [Map] structures using a path that may contain wildcards.
///
/// The maps must always use [String] keys.
@ -40,13 +31,18 @@ Iterable<({T value, String path})> selectInJsonWithPath<T>(
List<String> pathParts = path.split(".");
String first = pathParts.removeAt(0);
String rest = pathParts.join(".");
addFirstToPath(({T value, String path}) element) => (
value: element.value,
path: element.path.isEmpty ? first : "$first.${element.path}"
);
({T value, String path}) addFirstToPath(({T value, String path}) element) {
return (
value: element.value,
path: element.path.isEmpty ? first : "$first.${element.path}"
);
}
if (first == "*" || first == "**") {
String continueWithPath = first == "*" ? rest : path;
if (first == "**") {
yield* selectInJsonWithPath<T>(json, rest);
}
if (json is List) {
yield* json
.expand((e) => selectInJsonWithPath<T>(e, continueWithPath))

View File

@ -6,6 +6,4 @@ class MovieApi {
[];
Future<List<MovieData>> searchForMovies(String searchTerm) async => [];
Future<void> addMovieDetails(List<MovieData> movies) async {}
}

View File

@ -41,12 +41,6 @@ class WikidataMovieApi implements MovieApi {
ApiManager queryApi =
ApiManager("https://query.wikidata.org/sparql?format=json&origin=*");
@override
Future<void> addMovieDetails(List<MovieData> movies) {
// TODO: implement addMovieDetails
throw UnimplementedError();
}
@override
Future<List<WikidataMovieData>> getUpcomingMovies(DateTime startDate,
[int count = 100]) async {

View File

@ -1,48 +0,0 @@
/// Compares dates relative to each other. Times are ignored.
String dateRelativeToNow(DateTime date) {
DateTime dateOnly = DateTime.utc(date.year, date.month, date.day);
DateTime now = DateTime.now().toUtc();
DateTime today = DateTime.utc(now.year, now.month, now.day);
Duration diff = dateOnly.difference(today);
return _durationToRelativeDateString(diff);
}
String _durationToRelativeDateString(Duration duration) {
if (duration == const Duration(days: 1)) {
return "Tomorrow";
} else if (duration == const Duration(days: -1)) {
return "Yesterday";
}
if (duration.isNegative) {
String result = _durationApproximatedInWords(-duration);
return "${result[0].toUpperCase()}${result.substring(1)} ago";
} else if (duration == Duration.zero) {
return "Today";
} else {
return "In ${_durationApproximatedInWords(duration)}";
}
}
String _durationApproximatedInWords(Duration duration) {
int days = duration.inDays;
int weeks = (days / 7).floor();
int months = (days / 30).floor();
int years = (days / 365).floor();
int centuries = (years / 100).floor();
if (duration == Duration.zero) {
return "now";
}
if (days < 7) {
return days > 1 ? "$days days" : "a day";
}
if (months == 0) {
return weeks > 1 ? "$weeks weeks" : "a week";
}
if (years == 0) {
return months > 1 ? "$months months" : "a month";
}
if (years < 100) {
return years > 1 ? "$years years" : "a year";
}
return centuries > 1 ? "$centuries centuries" : "a century";
}

View File

@ -1,18 +1,25 @@
import 'package:get_storage/get_storage.dart';
import 'package:release_schedule/model/movie.dart';
class LocalMovieStorage {
abstract class LocalMovieStorage {
void update(List<MovieData> movies);
Future<List<MovieData>> retrieve();
}
class InMemoryMovieStorage implements LocalMovieStorage {
List<MovieData> _storedMovies = [];
@override
update(List<MovieData> movies) {
_storedMovies = movies;
}
@override
Future<List<MovieData>> retrieve() async {
return _storedMovies;
}
}
class LocalMovieStorageGetStorage extends LocalMovieStorage {
class LocalMovieStorageGetStorage implements LocalMovieStorage {
Future<void>? initialized;
GetStorage? container;
MovieData Function(Map jsonEncodable) toMovieData;

View File

@ -10,7 +10,6 @@ class MovieData extends ChangeNotifier {
List<DateWithPrecisionAndCountry>? _releaseDates;
List<String>? _genres;
List<TitleInLanguage>? _titles;
List<Review>? _reviews;
MovieData(this._title, this._releaseDate);
@ -38,10 +37,6 @@ class MovieData extends ChangeNotifier {
return _titles;
}
List<Review>? get reviews {
return _reviews;
}
bool get hasDetails {
return _hasDetails;
}
@ -54,8 +49,7 @@ class MovieData extends ChangeNotifier {
releaseDate: movie.releaseDate,
releaseDates: movie.releaseDates,
genres: movie.genres,
titles: movie.titles,
reviews: movie.reviews);
titles: movie.titles);
}
void setDetails(
@ -64,8 +58,7 @@ class MovieData extends ChangeNotifier {
bool? bookmarked,
List<DateWithPrecisionAndCountry>? releaseDates,
List<String>? genres,
List<TitleInLanguage>? titles,
List<Review>? reviews}) {
List<TitleInLanguage>? titles}) {
if (title != null) {
_title = title;
}
@ -84,16 +77,13 @@ class MovieData extends ChangeNotifier {
if (titles != null) {
_titles = titles;
}
if (reviews != null) {
_reviews = reviews;
}
_hasDetails = true;
notifyListeners();
}
@override
String toString() {
return "$title (${_releaseDate.toString()}${_genres?.isNotEmpty ?? true ? "; ${_genres?.join(", ")}" : ""})";
return "$title (${_releaseDate.toString()}${_genres?.isNotEmpty ?? false ? "; ${_genres?.join(", ")}" : ""})";
}
bool same(MovieData other) {
@ -112,7 +102,6 @@ class MovieData extends ChangeNotifier {
"releaseDates": releaseDatesByCountry,
"genres": genres,
"titles": titlesByCountry,
"reviews": reviews?.map((review) => review.toJsonEncodable()).toList(),
};
}
@ -131,11 +120,6 @@ class MovieData extends ChangeNotifier {
DateWithPrecisionAndCountry.fromJsonEncodable(release))
.toList()
: null,
reviews: json["reviews"] != null
? (json["reviews"] as List<dynamic>)
.map((review) => Review.fromJsonEncodable(review))
.toList()
: null,
titles: json["titles"] != null
? (json["titles"] as List<dynamic>)
.map((title) =>
@ -168,26 +152,3 @@ class DateWithPrecisionAndCountry {
return "${dateWithPrecision.toString()} ($country)";
}
}
class Review {
String score;
String by;
DateTime asOf;
int count;
Review(this.score, this.by, this.asOf, this.count);
Review.fromJsonEncodable(Map json)
: score = json["score"],
by = json["by"],
asOf = DateTime.parse(json["asOf"]),
count = json["count"];
Map toJsonEncodable() {
return {
"score": score,
"by": by,
"asOf": asOf.toIso8601String(),
"count": count,
};
}
}

View File

@ -45,7 +45,7 @@ class MovieManager extends ChangeNotifier {
bool added = false;
for (var movie in additionalMovies) {
MovieData? existing =
firstWhereOrNull(movies, (element) => movie.same(element));
movies.where((element) => movie.same(element)).firstOrNull;
if (existing == null) {
_insertMovie(movie);
movie.addListener(() {
@ -131,10 +131,6 @@ class MovieManager extends ChangeNotifier {
return addMovies(movies);
}
void expandDetails(List<MovieData> movies) {
api.addMovieDetails(movies);
}
Future<void> loadUpcomingMovies() async {
try {
loading = true;
@ -148,11 +144,3 @@ class MovieManager extends ChangeNotifier {
}
}
}
T? firstWhereOrNull<T>(List<T> list, bool Function(T element) test) {
try {
return list.firstWhere(test);
} catch (e) {
return null;
}
}

View File

@ -4,6 +4,9 @@ class Scored<T> {
T data;
double score;
Scored(this.data, this.score);
@override
toString() => '$data: $score';
}
List<T> searchList<T>(

View File

@ -1,84 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:release_schedule/model/date_format.dart';
void main() {
group('dateRelativeToNow', () {
test('returns "Today" for today\'s date', () {
final today = DateTime.now();
final result = dateRelativeToNow(today);
expect(result, 'Today');
});
test('returns "Tomorrow" for tomorrow\'s date', () {
final tomorrow = DateTime.now().add(const Duration(days: 1));
final result = dateRelativeToNow(tomorrow);
expect(result, 'Tomorrow');
});
test('returns "Yesterday" for yesterday\'s date', () {
final yesterday = DateTime.now().subtract(const Duration(days: 1));
final result = dateRelativeToNow(yesterday);
expect(result, 'Yesterday');
});
test('returns "In 5 days" for a date 5 days in the future', () {
final futureDate = DateTime.now().add(const Duration(days: 5));
final result = dateRelativeToNow(futureDate);
expect(result, 'In 5 days');
});
test('returns "5 days ago" for a date 5 days in the past', () {
final pastDate = DateTime.now().subtract(const Duration(days: 5));
final result = dateRelativeToNow(pastDate);
expect(result, '5 days ago');
});
test('returns "a week" for a date 7 days in the future', () {
final futureDate = DateTime.now().add(const Duration(days: 7));
final result = dateRelativeToNow(futureDate);
expect(result, 'In a week');
});
test('returns "a week" for a date 7 days in the past', () {
final pastDate = DateTime.now().subtract(const Duration(days: 7));
final result = dateRelativeToNow(pastDate);
expect(result, 'A week ago');
});
test('returns "a month" for a date 30 days in the future', () {
final futureDate = DateTime.now().add(const Duration(days: 30));
final result = dateRelativeToNow(futureDate);
expect(result, 'In a month');
});
test('returns "a month" for a date 30 days in the past', () {
final pastDate = DateTime.now().subtract(const Duration(days: 30));
final result = dateRelativeToNow(pastDate);
expect(result, 'A month ago');
});
test('returns "a year" for a date 365 days in the future', () {
final futureDate = DateTime.now().add(const Duration(days: 365));
final result = dateRelativeToNow(futureDate);
expect(result, 'In a year');
});
test('returns "a year" for a date 365 days in the past', () {
final pastDate = DateTime.now().subtract(const Duration(days: 365));
final result = dateRelativeToNow(pastDate);
expect(result, 'A year ago');
});
test('returns "a century" for a date 100 years in the future', () {
final futureDate = DateTime.now().add(const Duration(days: 365 * 100));
final result = dateRelativeToNow(futureDate);
expect(result, 'In a century');
});
test('returns "a century" for a date 100 years in the past', () {
final pastDate = DateTime.now().subtract(const Duration(days: 365 * 100));
final result = dateRelativeToNow(pastDate);
expect(result, 'A century ago');
});
});
}

View File

@ -0,0 +1,114 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:release_schedule/model/dates.dart';
void main() {
group("DatePrecisionComparison", () {
test("can compare with inequality", () {
expect(DatePrecision.decade < DatePrecision.year, isTrue);
expect(DatePrecision.year <= DatePrecision.year, isTrue);
expect(DatePrecision.month > DatePrecision.day, isFalse);
expect(DatePrecision.day > DatePrecision.day, isFalse);
expect(DatePrecision.hour >= DatePrecision.month, isTrue);
});
test("can compare with equality", () {
expect(DatePrecision.decade == DatePrecision.decade, isTrue);
expect(DatePrecision.year != DatePrecision.decade, isTrue);
});
});
test("simplifyDatesToPrecision", () {
expect(simplifyDateToPrecision(DateTime(2024, 5, 14), DatePrecision.decade),
equals(DateTime(2020, 1, 1)));
expect(simplifyDateToPrecision(DateTime(2024, 5, 14), DatePrecision.year),
equals(DateTime(2024, 1, 1)));
expect(simplifyDateToPrecision(DateTime(2024, 5, 14), DatePrecision.month),
equals(DateTime(2024, 5, 1)));
expect(
simplifyDateToPrecision(
DateTime(2024, 5, 14, 10, 42), DatePrecision.day),
equals(DateTime(2024, 5, 14)));
expect(
simplifyDateToPrecision(
DateTime(2024, 5, 14, 10, 42), DatePrecision.hour),
equals(DateTime(2024, 5, 14, 10)));
expect(
simplifyDateToPrecision(
DateTime(2024, 5, 14, 10, 42, 12), DatePrecision.minute),
equals(DateTime(2024, 5, 14, 10, 42)));
});
group("DateWithPrecision", () {
test("includes", () {
DateTime originalDate = DateTime(2024, 5, 14, 15, 42, 12);
expect(
DateWithPrecision(originalDate, DatePrecision.minute)
.includes(DateTime(2024, 5, 14, 15, 42, 12)),
isTrue);
expect(
DateWithPrecision(originalDate, DatePrecision.minute)
.includes(DateTime(2024, 5, 14, 15, 43, 1)),
isFalse);
expect(
DateWithPrecision(originalDate, DatePrecision.hour)
.includes(DateTime(2024, 5, 14, 15, 42, 12)),
isTrue);
expect(
DateWithPrecision(originalDate, DatePrecision.hour)
.includes(DateTime(2024, 5, 14, 16, 10, 12)),
isFalse);
expect(
DateWithPrecision(originalDate, DatePrecision.day)
.includes(DateTime(2024, 5, 14)),
isTrue);
expect(
DateWithPrecision(originalDate, DatePrecision.day)
.includes(DateTime(2024, 5, 15)),
isFalse);
expect(
DateWithPrecision(originalDate, DatePrecision.month)
.includes(DateTime(2024, 5, 20)),
isTrue);
expect(
DateWithPrecision(originalDate, DatePrecision.month)
.includes(DateTime(2024, 6, 10)),
isFalse);
expect(
DateWithPrecision(originalDate, DatePrecision.year)
.includes(DateTime(2024, 12, 31)),
isTrue);
expect(
DateWithPrecision(originalDate, DatePrecision.year)
.includes(DateTime(2025, 1, 1)),
isFalse);
expect(
DateWithPrecision(originalDate, DatePrecision.decade)
.includes(DateTime(2029, 12, 31)),
isTrue);
expect(
DateWithPrecision(originalDate, DatePrecision.decade)
.includes(DateTime(2020, 1, 1)),
isTrue);
expect(
DateWithPrecision(originalDate, DatePrecision.decade)
.includes(DateTime(2030, 1, 1)),
isFalse);
});
test("toString", () {
DateTime date = DateTime(2024, 5, 14, 15, 42, 12);
expect(DateWithPrecision(date, DatePrecision.minute).toString(),
equals("May 14, 2024, 15:42"));
expect(DateWithPrecision(date, DatePrecision.hour).toString(),
equals("May 14, 2024, 15"));
expect(DateWithPrecision(date, DatePrecision.day).toString(),
equals("May 14, 2024"));
expect(DateWithPrecision(date, DatePrecision.month).toString(),
equals("May 2024"));
expect(DateWithPrecision(date, DatePrecision.year).toString(),
equals("2024"));
expect(DateWithPrecision(date, DatePrecision.decade).toString(),
equals("2020s"));
});
});
}

View File

@ -0,0 +1,43 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:release_schedule/api/json_helper.dart';
void main() {
group("selectInJson", () {
late Map<String, dynamic> json;
setUp(() {
json = {
"a": {
"b": [
{"c": 1},
{"c": 2},
{"c": 3},
],
"c": 4,
},
"d": [
{"e": 5},
{"e": 6},
{"e": "7"},
{"e": 7},
]
};
});
test("should select a value", () {
expect(selectInJson<int>(json, "a.b.1.c").toList(), equals([2]));
});
test("should select multiple values", () {
expect(selectInJson<int>(json, "a.b.*.c").toList(), equals([1, 2, 3]));
});
test("should select multiple values with nested lists", () {
expect(selectInJson<int>(json, "a.**.c").toList(), equals([4, 1, 2, 3]));
});
test("should select multiple values with nested lists and maps", () {
expect(selectInJson<int>(json, "**.e").toList(), equals([5, 6, 7]));
});
});
}

View File

@ -12,7 +12,7 @@ void main() {
setUp(() {
movieManager = MovieManager(
MovieApi(),
LocalMovieStorage(),
InMemoryMovieStorage(),
);
});
@ -64,7 +64,38 @@ void main() {
expect(movieManager.movies, equals([...movies, ...newMovies]));
});
test("addMovies should sort movies by their release dates", () {
test('addMovies should update existing movies', () {
final movies = [
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
];
movieManager.addMovies(movies);
final updatedMovie = MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
)..setDetails(
bookmarked: true,
genres: ['Action', 'Adventure'],
);
movieManager.addMovies([updatedMovie]);
expect(movieManager.movies[1].genres, equals(updatedMovie.genres));
expect(movieManager.movies[1].bookmarked, equals(false));
});
test('addMovies should sort movies by their release dates', () {
final movies = [
MovieData(
'The Matrix Reloaded',
@ -83,6 +114,27 @@ void main() {
expect(movieManager.movies, equals([...movies.reversed]));
});
test(
'addMovies should sort movies that have a less precise release date before movies with more precise release dates',
() {
final movies = [
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.month,
'United States of America'),
),
];
movieManager.addMovies(movies);
expect(movieManager.movies, equals([...movies.reversed]));
});
test(
'when a movie is modified and it\'s date is changed the movies should be resorted',
() async {
@ -136,5 +188,28 @@ void main() {
expect(movieManager.movies, equals([notRemoved]));
});
test("localSearch", () {
final movies = [
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
];
movieManager.addMovies(movies);
expect(movieManager.localSearch('Matrix'), equals(movies));
expect(movieManager.localSearch('Matrix Re'),
equals(movies.reversed.toList()));
expect(movieManager.localSearch('Matrix Reloaded'), equals([movies[1]]));
expect(movieManager.localSearch('Matrix Revolutions'), equals([]));
});
});
}

View File

@ -21,8 +21,6 @@ void main() {
'Adventure'
], titles: [
(title: 'Title 2', language: 'en')
], reviews: [
Review('8.5', 'John Doe', DateTime(2023, 1, 1), 100)
]);
movie1.updateWithNewIgnoringUserControlled(movie2);
expect(movie1.title, equals('Title 2'));
@ -35,10 +33,6 @@ void main() {
expect(movie1.titles!.length, equals(1));
expect(movie1.titles![0].title, equals('Title 2'));
expect(movie1.titles![0].language, equals('en'));
expect(movie1.reviews!.length, equals(1));
expect(movie1.reviews![0].score, equals('8.5'));
expect(movie1.reviews![0].by, equals('John Doe'));
expect(movie1.reviews![0].count, equals(100));
});
test('same() returns true for same title and release date', () {
@ -89,8 +83,6 @@ void main() {
'Adventure'
], titles: [
(title: 'Title 2', language: 'en')
], reviews: [
Review('8.5', 'John Doe', DateTime(2023, 1, 1), 100)
]);
final json = movie.toJsonEncodable();
final movie2 = MovieData.fromJsonEncodable(json);
@ -104,10 +96,6 @@ void main() {
expect(movie2.titles!.length, equals(1));
expect(movie2.titles![0].title, equals('Title 2'));
expect(movie2.titles![0].language, equals('en'));
expect(movie2.reviews!.length, equals(1));
expect(movie2.reviews![0].score, equals('8.5'));
expect(movie2.reviews![0].by, equals('John Doe'));
expect(movie2.reviews![0].count, equals(100));
});
test('toString()', () {
@ -123,8 +111,6 @@ void main() {
'Adventure'
], titles: [
(title: 'Title 2', language: 'en')
], reviews: [
Review('8.5', 'John Doe', DateTime(2023, 1, 1), 100)
]);
expect(movie.toString(),
equals('Title 1 (January 1, 2023 (US); Action, Adventure)'));

View File

@ -9,14 +9,16 @@ import 'package:release_schedule/view/movie_manager_list.dart';
void main() {
group('HomePage', () {
testWidgets('displays title', (WidgetTester tester) async {
MovieManager movieManager = MovieManager(MovieApi(), LocalMovieStorage());
MovieManager movieManager =
MovieManager(MovieApi(), InMemoryMovieStorage());
await tester.pumpWidget(MaterialApp(home: HomePage(movieManager)));
expect(find.text('Search'), findsOneWidget);
});
testWidgets('displays list of releases', (WidgetTester tester) async {
MovieManager movieManager = MovieManager(MovieApi(), LocalMovieStorage());
MovieManager movieManager =
MovieManager(MovieApi(), InMemoryMovieStorage());
await tester.pumpWidget(MaterialApp(home: HomePage(movieManager)));
expect(find.byType(MovieManagerList), findsOneWidget);

View File

@ -12,7 +12,7 @@ import 'package:release_schedule/view/movie_manager_list.dart';
void main() {
group('MovieManagerList', () {
testWidgets('displays movie list', (tester) async {
final manager = MovieManager(MovieApi(), LocalMovieStorage());
final manager = MovieManager(MovieApi(), InMemoryMovieStorage());
manager.addMovies([
MovieData(
'Movie 1',
@ -34,7 +34,7 @@ void main() {
});
testWidgets('updates when new movies are added', (tester) async {
final manager = MovieManager(MovieApi(), LocalMovieStorage());
final manager = MovieManager(MovieApi(), InMemoryMovieStorage());
manager.addMovies([
MovieData(
'Movie 1',