Skip to content

Commit eaa1335

Browse files
committed
Added Inspector. SDK bump to 2.2.0
1 parent 60f6a3e commit eaa1335

File tree

4 files changed

+216
-5
lines changed

4 files changed

+216
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.3.0
2+
- Added Inspector: A tool for inspecting a collection of Sessions.
3+
- Bumped min SDK to 2.2.0 to support Set literal usage.
4+
15
## 2.2.1
26
- Cleaned out unneccessary internals.
37
- Deprecated Performance.wasTouched.

lib/src/inspector.dart

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import 'dart:io';
22

3-
//import 'package:traindown/src/metadata.dart';
3+
import 'package:traindown/src/metadata.dart';
44
import 'package:traindown/src/movement.dart';
5-
//import 'package:traindown/src/performance.dart';
5+
import 'package:traindown/src/performance.dart';
66
import 'package:traindown/src/session.dart';
7-
//import 'package:traindown/src/token.dart';
87

98
/// A search result that contains a DateTime and a Movement.
109
class MovementSearchResult {
@@ -13,6 +12,9 @@ class MovementSearchResult {
1312
MovementSearchResult(this.occurred, this.movement);
1413
}
1514

15+
/// Scope levels useful for querying
16+
enum TraindownScope { session, movement, performance }
17+
1618
/// Inspector provides tools for analyzing Sessions.
1719
class Inspector {
1820
List<Session> sessions;
@@ -48,6 +50,14 @@ class Inspector {
4850
return Inspector.from_files(files);
4951
}
5052

53+
/// Stack rank of scopes in terms of depth priority.
54+
static const Map<TraindownScope, int> ScopePriority = {
55+
TraindownScope.session: 0,
56+
TraindownScope.movement: 1,
57+
TraindownScope.performance: 2
58+
};
59+
60+
/// Filters out non Traindown files.
5161
bool validFile(File file) {
5262
bool valid = false;
5363

@@ -61,6 +71,42 @@ class Inspector {
6171
return valid;
6272
}
6373

74+
/// Map keyed by string that contains all seen values of the given key.
75+
Map<String, Set<String>> metadataByKey(
76+
[TraindownScope scope = TraindownScope.session]) {
77+
Map<String, Set<String>> result = {};
78+
79+
for (Session session in sessions) {
80+
Metadata metadata = session.metadata;
81+
for (String key in metadata.kvps.keys) {
82+
result[key] ??= <String>{};
83+
result[key].add(metadata.kvps[key]);
84+
}
85+
86+
if (ScopePriority[scope] > ScopePriority[TraindownScope.session]) {
87+
for (Movement movement in session.movements) {
88+
metadata = movement.metadata;
89+
for (String key in metadata.kvps.keys) {
90+
result[key] ??= <String>{};
91+
result[key].add(metadata.kvps[key]);
92+
}
93+
94+
if (ScopePriority[scope] > ScopePriority[TraindownScope.movement]) {
95+
for (Performance performance in movement.performances) {
96+
metadata = performance.metadata;
97+
for (String key in metadata.kvps.keys) {
98+
result[key] ??= <String>{};
99+
result[key].add(metadata.kvps[key]);
100+
}
101+
}
102+
}
103+
}
104+
}
105+
}
106+
107+
return result;
108+
}
109+
64110
/// List of unique Movement names in the Sessions.
65111
List<String> get movementNames => sessions
66112
.expand((s) => s.movements.map((m) => m.name.toLowerCase()))
@@ -81,4 +127,34 @@ class Inspector {
81127

82128
return result;
83129
}
130+
131+
/// List of Sessions matching the search criteria.
132+
/// [metaLike] will match against all key value pairs provided in a case
133+
/// insensitve manner.
134+
// TODO: Add date filters
135+
List<Session> sessionQuery({Map<String, String> metaLike = const {}}) {
136+
List<Session> result = sessions;
137+
138+
if (metaLike.isNotEmpty) {
139+
metaLike = metaLike.map((k, v) => MapEntry(k.toLowerCase(), v));
140+
result = result.where((s) {
141+
if (s.kvps.isEmpty) return false;
142+
Map<String, String> lowerMeta =
143+
s.kvps.map((k, v) => MapEntry(k.toLowerCase(), v));
144+
145+
bool match = false;
146+
for (String key in lowerMeta.keys) {
147+
if (match) break;
148+
if (!metaLike.keys.contains(key)) continue;
149+
150+
match = (metaLike[key] ?? "NOPE").toLowerCase() ==
151+
(lowerMeta[key] ?? "YUP").toLowerCase();
152+
}
153+
154+
return match;
155+
}).toList();
156+
}
157+
158+
return result;
159+
}
84160
}

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: traindown
22
description: A language to help athletes express their training. Inspired by Markdown.
33
homepage: https://www.traindown.com/
4-
version: 2.2.1
4+
version: 2.3.0
55
environment:
6-
sdk: ">=2.0.0 <3.0.0"
6+
sdk: ">=2.2.0 <3.0.0"
77
dependencies:
88
characters: ^1.0.0
99
dev_dependencies:

test/inspector_test.dart

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,44 @@ void main() {
3333
});
3434
});
3535

36+
group('metadataByKey', () {
37+
List<File> files = [File('./example/example.traindown')];
38+
Inspector subject = Inspector.from_files(files);
39+
40+
Map<String, Set<String>> expected = {
41+
"bw": {"230"},
42+
"session meta key 1": {"session meta \"value\" 1"},
43+
"session meta key '2'": {"session meta value 2"},
44+
"3rd session meta key": {"3rd session meta value"},
45+
};
46+
47+
test('Session scope', () {
48+
Map<String, Set<String>> meta = subject.metadataByKey();
49+
expect(meta.length, 4);
50+
expect(meta, equals(expected));
51+
});
52+
53+
test('Movement scope', () {
54+
expected["2nd movement meta key 1"] = {"2nd movement meta value 1"};
55+
expected["Movement 2 meta key 2"] = {"Movement 2 meta key 2"};
56+
Map<String, Set<String>> meta =
57+
subject.metadataByKey(TraindownScope.movement);
58+
expect(meta.length, 6);
59+
expect(meta, equals(expected));
60+
});
61+
62+
test('Performance scope', () {
63+
expected["2nd movement meta key 1"] = {"2nd movement meta value 1"};
64+
expected["Movement 2 meta key 2"] = {"Movement 2 meta key 2"};
65+
expected["302 meta key 1"] = {"302 meta value 1"};
66+
expected["Meta key 2 for 302"] = {"Meta value 2 for 302"};
67+
Map<String, Set<String>> meta =
68+
subject.metadataByKey(TraindownScope.performance);
69+
expect(meta.length, 8);
70+
expect(meta, equals(expected));
71+
});
72+
});
73+
3674
group('movementNames', () {
3775
test('Basic use', () {
3876
List<File> files = [File('./example/example.traindown')];
@@ -83,6 +121,99 @@ void main() {
83121
});
84122
});
85123

124+
group('sessionQuery', () {
125+
List<Token> tokens1 = [
126+
Token(TokenType.DateTime, "2020-01-01"),
127+
Token(TokenType.MetaKey, "Your"),
128+
Token(TokenType.MetaValue, "Mom"),
129+
Token(TokenType.MetaKey, "Foo"),
130+
Token(TokenType.MetaValue, "Bar"),
131+
];
132+
Session session1 = Session(tokens1);
133+
134+
List<Token> tokens2 = [
135+
Token(TokenType.DateTime, "2021-01-01"),
136+
Token(TokenType.MetaKey, "Your"),
137+
Token(TokenType.MetaValue, "Mom"),
138+
Token(TokenType.MetaKey, "Bar"),
139+
Token(TokenType.MetaValue, "Baz"),
140+
];
141+
Session session2 = Session(tokens2);
142+
143+
Inspector subject = Inspector([session1, session2]);
144+
145+
test('One kvp that does not match key or value', () {
146+
expect(subject.sessionQuery(metaLike: {"test": "test"}), equals([]));
147+
});
148+
149+
test('One kvp that does match key but not value', () {
150+
expect(subject.sessionQuery(metaLike: {"Your": "test"}), equals([]));
151+
});
152+
153+
test('One kvp that does match key and value on single', () {
154+
List<Session> results = subject.sessionQuery(metaLike: {"Foo": "Bar"});
155+
expect(results.length, 1);
156+
expect(results.contains(session1), true);
157+
});
158+
159+
test('One kvp that does match key and value on multiple', () {
160+
List<Session> results = subject.sessionQuery(metaLike: {"Your": "Mom"});
161+
expect(results.length, 2);
162+
expect(results.contains(session1), true);
163+
expect(results.contains(session2), true);
164+
});
165+
166+
test('One kvp that does match key and value on single case insensitive key',
167+
() {
168+
List<Session> results = subject.sessionQuery(metaLike: {"FOO": "Bar"});
169+
expect(results.length, 1);
170+
expect(results.contains(session1), true);
171+
});
172+
173+
test(
174+
'One kvp that does match key and value on multiple case insensitive key',
175+
() {
176+
List<Session> results = subject.sessionQuery(metaLike: {"YOUR": "Mom"});
177+
expect(results.length, 2);
178+
expect(results.contains(session1), true);
179+
expect(results.contains(session2), true);
180+
});
181+
182+
test(
183+
'One kvp that does match key and value on single case insensitive value',
184+
() {
185+
List<Session> results = subject.sessionQuery(metaLike: {"Foo": "BAR"});
186+
expect(results.length, 1);
187+
expect(results.contains(session1), true);
188+
});
189+
190+
test(
191+
'One kvp that does match key and value on multiple case insensitive value',
192+
() {
193+
List<Session> results = subject.sessionQuery(metaLike: {"Your": "MOM"});
194+
expect(results.length, 2);
195+
expect(results.contains(session1), true);
196+
expect(results.contains(session2), true);
197+
});
198+
199+
test(
200+
'One kvp that does match key and value on single case insensitive key and value',
201+
() {
202+
List<Session> results = subject.sessionQuery(metaLike: {"FOO": "BAR"});
203+
expect(results.length, 1);
204+
expect(results.contains(session1), true);
205+
});
206+
207+
test(
208+
'One kvp that does match key and value on multiple case insensitive key and value',
209+
() {
210+
List<Session> results = subject.sessionQuery(metaLike: {"YOUR": "MOM"});
211+
expect(results.length, 2);
212+
expect(results.contains(session1), true);
213+
expect(results.contains(session2), true);
214+
});
215+
});
216+
86217
group('validFile', () {
87218
test('It returns true for allowed extensions', () {
88219
Inspector subject = Inspector([], ['.traindown']);

0 commit comments

Comments
 (0)