5053 can get points from lemma with max score (#5078)
* make uses a private field for ConstructUses * expose capped list of uses in ConstructUses * filter capped construct uses in getUses
This commit is contained in:
parent
0ba50f9d73
commit
2c176c052d
10 changed files with 164 additions and 146 deletions
|
|
@ -241,7 +241,23 @@ class AnalyticsDataService {
|
|||
);
|
||||
|
||||
final blocked = blockedConstructs;
|
||||
return uses.where((use) => !blocked.contains(use.identifier)).toList();
|
||||
final List<OneConstructUse> filtered = [];
|
||||
|
||||
final Map<ConstructIdentifier, DateTime?> cappedLastUseCache = {};
|
||||
for (final use in uses) {
|
||||
if (blocked.contains(use.identifier)) continue;
|
||||
if (!cappedLastUseCache.containsKey(use.identifier)) {
|
||||
final constructs = await getConstructUse(use.identifier);
|
||||
cappedLastUseCache[use.identifier] = constructs.cappedLastUse;
|
||||
}
|
||||
final cappedLastUse = cappedLastUseCache[use.identifier];
|
||||
if (cappedLastUse != null && use.timeStamp.isAfter(cappedLastUse)) {
|
||||
continue;
|
||||
}
|
||||
filtered.add(use);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> getLocalUses() async {
|
||||
|
|
|
|||
|
|
@ -199,82 +199,55 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
DateTime? since,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final List<OneConstructUse> uses = [];
|
||||
final results = <OneConstructUse>[];
|
||||
|
||||
// first, get all of the local (most recent) keys
|
||||
final localKeys = await _localConstructsBox.getAllKeys();
|
||||
final localValues = await _localConstructsBox.getAll(localKeys);
|
||||
final local = Map.fromIterables(
|
||||
localKeys,
|
||||
localValues,
|
||||
).entries.toList();
|
||||
|
||||
local.sort(
|
||||
(a, b) => int.parse(b.key).compareTo(int.parse(a.key)),
|
||||
);
|
||||
|
||||
for (final entry in local) {
|
||||
// filter by date
|
||||
if (since != null &&
|
||||
int.parse(entry.key) < since.millisecondsSinceEpoch) {
|
||||
continue;
|
||||
bool addUseIfValid(OneConstructUse use) {
|
||||
if (since != null && use.timeStamp.isBefore(since)) {
|
||||
return false; // stop iteration entirely
|
||||
}
|
||||
if (roomId != null && use.metadata.roomId != roomId) {
|
||||
return true; // skip but continue
|
||||
}
|
||||
|
||||
final rawUses = entry.value;
|
||||
if (rawUses == null) continue;
|
||||
for (final raw in rawUses) {
|
||||
// filter by count
|
||||
if (count != null && uses.length >= count) break;
|
||||
|
||||
final use = OneConstructUse.fromJson(
|
||||
Map<String, dynamic>.from(raw),
|
||||
);
|
||||
|
||||
// filter by roomID
|
||||
if (roomId != null && use.metadata.roomId != roomId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uses.add(use);
|
||||
}
|
||||
if (count != null && uses.length >= count) break;
|
||||
results.add(use);
|
||||
return count == null || results.length < count;
|
||||
}
|
||||
if (count != null && uses.length >= count) return uses;
|
||||
|
||||
// then get server uses
|
||||
final serverKeys = await _serverConstructsBox.getAllKeys();
|
||||
serverKeys.sort(
|
||||
(a, b) =>
|
||||
int.parse(b.split('|')[1]).compareTo(int.parse(a.split('|')[1])),
|
||||
);
|
||||
// ---- Local uses ----
|
||||
final localUses = await getLocalUses()
|
||||
..sort((a, b) => b.timeStamp.compareTo(a.timeStamp));
|
||||
|
||||
for (final use in localUses) {
|
||||
if (!addUseIfValid(use)) break;
|
||||
}
|
||||
|
||||
if (count != null && results.length >= count) {
|
||||
stopwatch.stop();
|
||||
Logs().i("Get uses took ${stopwatch.elapsedMilliseconds} ms");
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---- Server uses ----
|
||||
final serverKeys = await _serverConstructsBox.getAllKeys()
|
||||
..sort(
|
||||
(a, b) =>
|
||||
int.parse(b.split('|')[1]).compareTo(int.parse(a.split('|')[1])),
|
||||
);
|
||||
|
||||
for (final key in serverKeys) {
|
||||
// filter by count
|
||||
if (count != null && uses.length >= count) break;
|
||||
final rawUses = await _serverConstructsBox.get(key);
|
||||
if (rawUses == null) continue;
|
||||
for (final raw in rawUses) {
|
||||
if (count != null && uses.length >= count) break;
|
||||
final use = OneConstructUse.fromJson(
|
||||
Map<String, dynamic>.from(raw),
|
||||
);
|
||||
final serverUses = await getServerUses(key)
|
||||
..sort((a, b) => b.timeStamp.compareTo(a.timeStamp));
|
||||
|
||||
// filter by roomID
|
||||
if (roomId != null && use.metadata.roomId != roomId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter by date
|
||||
if (since != null && use.timeStamp.isBefore(since)) {
|
||||
continue;
|
||||
}
|
||||
uses.add(use);
|
||||
for (final use in serverUses) {
|
||||
if (!addUseIfValid(use)) break;
|
||||
}
|
||||
|
||||
if (count != null && results.length >= count) break;
|
||||
}
|
||||
|
||||
stopwatch.stop();
|
||||
Logs().i("Get uses took ${stopwatch.elapsedMilliseconds} ms");
|
||||
|
||||
return uses.take(count ?? uses.length).toList();
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> getLocalUses() async {
|
||||
|
|
@ -293,6 +266,21 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
return uses;
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> getServerUses(String key) async {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final serverValues = await _serverConstructsBox.get(key);
|
||||
if (serverValues == null) return [];
|
||||
|
||||
for (final entry in serverValues) {
|
||||
uses.add(
|
||||
OneConstructUse.fromJson(
|
||||
Map<String, dynamic>.from(entry),
|
||||
),
|
||||
);
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
Future<int> getLocalConstructCount() async {
|
||||
final keys = await _localConstructsBox.getAllKeys();
|
||||
return keys.length;
|
||||
|
|
@ -408,8 +396,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
}
|
||||
|
||||
for (final u in usesForKey) {
|
||||
model.uses.add(u);
|
||||
model.setLastUsed(u.timeStamp);
|
||||
model.addUse(u);
|
||||
}
|
||||
|
||||
updates[key] = model;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ class ConstructMergeTable {
|
|||
List<ConstructUses> constructs,
|
||||
Set<ConstructIdentifier> exclude,
|
||||
) {
|
||||
addConstructsByUses(constructs.expand((c) => c.uses).toList(), exclude);
|
||||
addConstructsByUses(
|
||||
constructs.expand((c) => c.cappedUses).toList(),
|
||||
exclude,
|
||||
);
|
||||
}
|
||||
|
||||
void addConstructsByUses(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class LemmaUsageDots extends StatelessWidget {
|
|||
/// Find lemma uses for the given exercise type, to create dot list
|
||||
List<bool> sortedUses(LearningSkillsEnum category) {
|
||||
final List<bool> useList = [];
|
||||
for (final OneConstructUse use in construct.uses) {
|
||||
for (final OneConstructUse use in construct.cappedUses) {
|
||||
if (use.xp == 0) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class LemmaUseExampleMessages extends StatelessWidget {
|
|||
|
||||
Future<List<ExampleMessage>> _getExampleMessages() async {
|
||||
final List<ExampleMessage> examples = [];
|
||||
for (final OneConstructUse use in construct.uses) {
|
||||
for (final OneConstructUse use in construct.cappedUses) {
|
||||
if (use.useType.skillsEnumType != LearningSkillsEnum.writing ||
|
||||
use.metadata.eventId == null ||
|
||||
use.form == null ||
|
||||
|
|
|
|||
|
|
@ -65,13 +65,7 @@ class VocabDetailsView extends StatelessWidget {
|
|||
? level.color(context)
|
||||
: level.darkColor(context));
|
||||
|
||||
final forms = construct?.uses
|
||||
.map((e) => e.form)
|
||||
.whereType<String>()
|
||||
.toSet()
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final forms = construct?.forms ?? [];
|
||||
final tokenText = PangeaTokenText.fromString(constructId.lemma);
|
||||
final token = PangeaToken(
|
||||
text: tokenText,
|
||||
|
|
|
|||
|
|
@ -165,7 +165,8 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
|
||||
final xp = uses.map((e) => e.points).reduce((a, total) => a + total);
|
||||
final exampleMessages = await _getExampleMessages(uses);
|
||||
final allUses = uses.map((u) => u.uses).expand((element) => element);
|
||||
final allUses =
|
||||
uses.map((u) => u.cappedUses).expand((element) => element);
|
||||
|
||||
int independantUseOccurrences = 0;
|
||||
int assistedUseOccurrences = 0;
|
||||
|
|
@ -218,7 +219,7 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
|
||||
final xp = uses.points;
|
||||
final exampleMessages = await _getExampleMessages([uses]);
|
||||
final allUses = uses.uses;
|
||||
final allUses = uses.cappedUses;
|
||||
|
||||
int independantUseOccurrences = 0;
|
||||
int assistedUseOccurrences = 0;
|
||||
|
|
@ -261,7 +262,8 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
Future<List<String>> _getExampleMessages(
|
||||
List<ConstructUses> constructUses,
|
||||
) async {
|
||||
final allUses = constructUses.map((e) => e.uses).expand((e) => e).toList();
|
||||
final allUses =
|
||||
constructUses.map((e) => e.cappedUses).expand((e) => e).toList();
|
||||
final List<PangeaMessageEvent> examples = [];
|
||||
for (final OneConstructUse use in allUses) {
|
||||
if (use.metadata.roomId == null) continue;
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ class SpaceAnalyticsSummaryModel {
|
|||
final systemUsesCorrect = [];
|
||||
final systemUsesIncorrect = [];
|
||||
|
||||
for (final use in entry.uses) {
|
||||
for (final use in entry.cappedUses) {
|
||||
if (originalUseTypes.contains(use.useType)) {
|
||||
use.xp > 0
|
||||
? originalUsesCorrect.add(use)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,35 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
|
||||
/// One lemma and a list of construct uses for that lemma
|
||||
class ConstructUses {
|
||||
final List<OneConstructUse> uses;
|
||||
final List<OneConstructUse> _uses;
|
||||
final ConstructTypeEnum constructType;
|
||||
final String lemma;
|
||||
String? _category;
|
||||
DateTime? _lastUsed;
|
||||
|
||||
ConstructUses({
|
||||
required this.uses,
|
||||
required List<OneConstructUse> uses,
|
||||
required this.constructType,
|
||||
required this.lemma,
|
||||
required category,
|
||||
}) : _category = category;
|
||||
}) : _category = category,
|
||||
_uses = List<OneConstructUse>.from(uses) {
|
||||
_sortUses();
|
||||
}
|
||||
|
||||
// Total points for all uses of this lemma
|
||||
int get points {
|
||||
return min(
|
||||
uses.fold<int>(
|
||||
_uses.fold<int>(
|
||||
0,
|
||||
(total, use) => total + use.xp,
|
||||
),
|
||||
|
|
@ -32,28 +37,16 @@ class ConstructUses {
|
|||
);
|
||||
}
|
||||
|
||||
DateTime? get lastUsed {
|
||||
if (_lastUsed != null) return _lastUsed;
|
||||
final lastUse = uses.fold<DateTime?>(null, (DateTime? last, use) {
|
||||
if (last == null) return use.timeStamp;
|
||||
return use.timeStamp.isAfter(last) ? use.timeStamp : last;
|
||||
});
|
||||
return _lastUsed = lastUse;
|
||||
}
|
||||
|
||||
void setLastUsed(DateTime time) {
|
||||
if (_lastUsed == null || time.isAfter(_lastUsed!)) {
|
||||
_lastUsed = time;
|
||||
}
|
||||
}
|
||||
DateTime? get lastUsed => _uses.lastOrNull?.timeStamp;
|
||||
DateTime? get cappedLastUse => cappedUses.lastOrNull?.timeStamp;
|
||||
|
||||
String get category {
|
||||
if (_category == null || _category!.isEmpty) return "other";
|
||||
return _category!.toLowerCase();
|
||||
}
|
||||
|
||||
bool get hasCorrectUse => uses.any((use) => use.xp > 0);
|
||||
bool get hasIncorrectUse => uses.any((use) => use.xp < 0);
|
||||
bool get hasCorrectUse => _uses.any((use) => use.xp > 0);
|
||||
bool get hasIncorrectUse => _uses.any((use) => use.xp < 0);
|
||||
|
||||
ConstructIdentifier get id => ConstructIdentifier(
|
||||
lemma: lemma,
|
||||
|
|
@ -61,38 +54,6 @@ class ConstructUses {
|
|||
category: category,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {
|
||||
'construct_id': id.toJson(),
|
||||
'xp': points,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
'uses': uses.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
||||
factory ConstructUses.fromJson(Map<String, dynamic> json) {
|
||||
final constructId = ConstructIdentifier.fromJson(
|
||||
Map<String, dynamic>.from(json['construct_id']),
|
||||
);
|
||||
|
||||
List<dynamic> usesJson = [];
|
||||
if (json['uses'] is List) {
|
||||
usesJson = List<dynamic>.from(json['uses']);
|
||||
}
|
||||
|
||||
final uses = usesJson
|
||||
.map((e) => OneConstructUse.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList();
|
||||
|
||||
return ConstructUses(
|
||||
uses: uses,
|
||||
constructType: constructId.type,
|
||||
lemma: constructId.lemma,
|
||||
category: constructId.category,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the lemma category, based on points
|
||||
ConstructLevelEnum get lemmaCategory {
|
||||
if (points < AnalyticsConstants.xpForGreens) {
|
||||
|
|
@ -122,6 +83,66 @@ class ConstructUses {
|
|||
_ => ConstructLevelEnum.flowers,
|
||||
};
|
||||
|
||||
List<String> get forms =>
|
||||
_uses.map((e) => e.form).whereType<String>().toSet().toList();
|
||||
|
||||
List<OneConstructUse> get cappedUses {
|
||||
final result = <OneConstructUse>[];
|
||||
var totalXp = 0;
|
||||
|
||||
for (final use in _uses) {
|
||||
if (totalXp >= AnalyticsConstants.xpForFlower) break;
|
||||
totalXp += use.xp;
|
||||
result.add(use);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
DateTime? lastUseByTypes(List<ConstructUseTypeEnum> types) =>
|
||||
_uses.lastWhereOrNull((u) => types.contains(u.useType))?.timeStamp;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {
|
||||
'construct_id': id.toJson(),
|
||||
'xp': points,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
'uses': _uses.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
||||
factory ConstructUses.fromJson(Map<String, dynamic> json) {
|
||||
final constructId = ConstructIdentifier.fromJson(
|
||||
Map<String, dynamic>.from(json['construct_id']),
|
||||
);
|
||||
|
||||
List<dynamic> usesJson = [];
|
||||
if (json['uses'] is List) {
|
||||
usesJson = List<dynamic>.from(json['uses']);
|
||||
}
|
||||
|
||||
final uses = usesJson
|
||||
.map((e) => OneConstructUse.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList();
|
||||
|
||||
return ConstructUses(
|
||||
uses: uses,
|
||||
constructType: constructId.type,
|
||||
lemma: constructId.lemma,
|
||||
category: constructId.category,
|
||||
);
|
||||
}
|
||||
|
||||
void _sortUses() {
|
||||
_uses.sort((a, b) => a.timeStamp.compareTo(b.timeStamp));
|
||||
}
|
||||
|
||||
void addUse(OneConstructUse use) {
|
||||
_uses.add(use);
|
||||
_sortUses();
|
||||
}
|
||||
|
||||
void merge(ConstructUses other) {
|
||||
if (other.lemma.toLowerCase() != lemma.toLowerCase() ||
|
||||
other.constructType != constructType) {
|
||||
|
|
@ -130,10 +151,8 @@ class ConstructUses {
|
|||
);
|
||||
}
|
||||
|
||||
uses.addAll(other.uses);
|
||||
if (other.lastUsed != null) {
|
||||
setLastUsed(other.lastUsed!);
|
||||
}
|
||||
_uses.addAll(other._uses);
|
||||
_sortUses();
|
||||
|
||||
if (category == 'other' && other.category != 'other') {
|
||||
_category = other.category;
|
||||
|
|
@ -147,7 +166,7 @@ class ConstructUses {
|
|||
String? category,
|
||||
}) {
|
||||
return ConstructUses(
|
||||
uses: uses ?? this.uses,
|
||||
uses: uses ?? _uses,
|
||||
constructType: constructType ?? this.constructType,
|
||||
lemma: lemma ?? this.lemma,
|
||||
category: category ?? _category,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
|
@ -242,13 +241,11 @@ class PracticeSelectionRepo {
|
|||
|
||||
for (final token in tokens) {
|
||||
final construct = constructs[idMap[token]];
|
||||
final lastUsed = construct?.uses.firstWhereOrNull(
|
||||
(u) => activityType.associatedUseTypes.contains(u.useType),
|
||||
);
|
||||
final lastUsed =
|
||||
construct?.lastUseByTypes(activityType.associatedUseTypes);
|
||||
|
||||
final daysSinceLastUsed = lastUsed == null
|
||||
? 20
|
||||
: DateTime.now().difference(lastUsed.timeStamp).inDays;
|
||||
final daysSinceLastUsed =
|
||||
lastUsed == null ? 20 : DateTime.now().difference(lastUsed).inDays;
|
||||
|
||||
scores[token] =
|
||||
daysSinceLastUsed * (token.vocabConstructID.isContentWord ? 10 : 9);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue