Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
33bee511e5 | ||
![]() |
2fe725134a | ||
6d0335b8d9 |
@@ -16,7 +16,6 @@ COPY ./management/build.gradle.kts ./management/
|
|||||||
COPY ./server/build.gradle.kts ./server/
|
COPY ./server/build.gradle.kts ./server/
|
||||||
COPY ./bots/ping-bot/build.gradle.kts ./bots/ping-bot/
|
COPY ./bots/ping-bot/build.gradle.kts ./bots/ping-bot/
|
||||||
COPY ./bots/pizza-bot/build.gradle.kts ./bots/pizza-bot/
|
COPY ./bots/pizza-bot/build.gradle.kts ./bots/pizza-bot/
|
||||||
COPY ./bots/sleep-tracker/build.gradle.kts ./bots/sleep-tracker/
|
|
||||||
RUN ./gradlew deps --no-daemon --info --stacktrace
|
RUN ./gradlew deps --no-daemon --info --stacktrace
|
||||||
|
|
||||||
# Build Application
|
# Build Application
|
||||||
|
@@ -23,12 +23,14 @@ import java.sql.SQLException;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BinaryOperator;
|
import java.util.function.BinaryOperator;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static eu.jonahbauer.chat.bot.pizza.util.ArgumentParser.*;
|
import static eu.jonahbauer.chat.bot.pizza.util.ArgumentParser.*;
|
||||||
import static eu.jonahbauer.chat.bot.pizza.util.FormatUtils.*;
|
import static eu.jonahbauer.chat.bot.pizza.util.FormatUtils.*;
|
||||||
|
import static java.util.FormatProcessor.FMT;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class PizzaBot extends ChatBot {
|
public class PizzaBot extends ChatBot {
|
||||||
@@ -326,20 +328,23 @@ public class PizzaBot extends ChatBot {
|
|||||||
var lines = new ArrayList<String>(items.size() + 2);
|
var lines = new ArrayList<String>(items.size() + 2);
|
||||||
lines.add("Übersicht " + name);
|
lines.add("Übersicht " + name);
|
||||||
lines.add("");
|
lines.add("");
|
||||||
if (items.isEmpty()) lines.add("(keine Einträge)");
|
if (items.isEmpty()) {
|
||||||
items.forEach((item, users) -> {
|
lines.add("(keine Einträge)");
|
||||||
var counts = users.stream().collect(Collectors.groupingBy(
|
} else {
|
||||||
Function.identity(),
|
items.forEach((item, users) -> {
|
||||||
Collectors.counting()
|
var counts = users.stream().collect(Collectors.groupingBy(
|
||||||
));
|
Function.identity(),
|
||||||
var usersString = counts.entrySet().stream()
|
Collectors.counting()
|
||||||
.sorted(Map.Entry.comparingByKey())
|
));
|
||||||
.map(entry -> entry.getValue() == 1 ? entry.getKey() : entry.getValue() + "x " + entry.getKey())
|
var usersString = counts.entrySet().stream()
|
||||||
.collect(Collectors.joining(", "));
|
.sorted(Map.Entry.comparingByKey())
|
||||||
var pizza = item.first().name().toUpperCase(Locale.ROOT);
|
.map(entry -> entry.getValue() == 1 ? entry.getKey() : entry.getValue() + "x " + entry.getKey())
|
||||||
var notes = item.second() != null ? " " + item.second() : "";
|
.collect(Collectors.joining(", "));
|
||||||
lines.add(STR."\{tabular(users.size())}x \{pizza}\{notes} (\{usersString})");
|
var pizza = item.first().name().toUpperCase(Locale.ROOT);
|
||||||
});
|
var notes = item.second() != null ? " " + item.second() : "";
|
||||||
|
lines.add(STR."\{tabular(users.size())}x \{pizza}\{notes} (\{usersString})");
|
||||||
|
});
|
||||||
|
}
|
||||||
post(String.join("\n", lines));
|
post(String.join("\n", lines));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,27 +405,36 @@ public class PizzaBot extends ChatBot {
|
|||||||
lines.add("Rechnung für " + name + (tipped ? " (mit Trinkgeld)" : ""));
|
lines.add("Rechnung für " + name + (tipped ? " (mit Trinkgeld)" : ""));
|
||||||
lines.add("alle Angaben ohne Gewähr");
|
lines.add("alle Angaben ohne Gewähr");
|
||||||
lines.add("");
|
lines.add("");
|
||||||
items.forEach((user, receipt) -> {
|
if (items.isEmpty()) {
|
||||||
if (receipt.payed()) {
|
lines.add("(keine Einträge)");
|
||||||
lines.add(strikethrough(user.username() + ": " + receipt.totalAsString()));
|
} else {
|
||||||
} else if (receipt.unpayed()) {
|
var toString = (BiConsumer<String, Receipt>) (user, receipt) -> {
|
||||||
lines.add(user.username() + ": " + receipt.totalAsString());
|
if (receipt.payed()) {
|
||||||
} else {
|
lines.add(strikethrough(user + ": " + receipt.totalAsString()));
|
||||||
lines.add(user.username() + ": " + strikethrough(receipt.totalAsString()) + " " + receipt.pendingAsString());
|
} else if (receipt.unpayed()) {
|
||||||
}
|
lines.add(user + ": " + receipt.totalAsString());
|
||||||
});
|
} else {
|
||||||
|
lines.add(user + ": " + strikethrough(receipt.totalAsString()) + " " + receipt.pendingAsString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
items.forEach((user, receipt) -> toString.accept(user.username(), receipt));
|
||||||
|
lines.add("");
|
||||||
|
toString.accept("Gesamt", items.values().stream().reduce(Receipt::sum).orElseThrow());
|
||||||
|
}
|
||||||
post(String.join("\n", lines));
|
post(String.join("\n", lines));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pay(double amount) throws SQLException {
|
private void pay(double amount) throws SQLException {
|
||||||
var order = service.getCurrentOrder().checkAdminAccess(getUser());
|
var order = service.getCurrentOrder().checkAdminAccess(getUser());
|
||||||
service.save(order.order().withPayment(amount));
|
service.save(order.order().withPayment(amount));
|
||||||
|
post(FMT."Zahlung von %.2f\{amount} für \{order.name()} wurde gespeichert");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pay(@NotNull String name, double amount) throws SQLException {
|
private void pay(@NotNull String name, double amount) throws SQLException {
|
||||||
var order = service.getCurrentOrder().checkAdminAccess(getUser());
|
var order = service.getCurrentOrder().checkAdminAccess(getUser());
|
||||||
var other = service.getOrderByEventAndName(order.eventId(), name);
|
var other = service.getOrderByEventAndName(order.eventId(), name);
|
||||||
service.save(other.withPayment(amount));
|
service.save(other.withPayment(amount));
|
||||||
|
post(FMT."Zahlung von %.2f\{amount} für \{other.name()} wurde gespeichert");
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
|
@@ -45,9 +45,8 @@ public class FormatUtils {
|
|||||||
public static @NotNull String strikethrough(@NotNull String string) {
|
public static @NotNull String strikethrough(@NotNull String string) {
|
||||||
var out = new StringBuilder();
|
var out = new StringBuilder();
|
||||||
for (int i = 0, length = string.length(); i < length; i++) {
|
for (int i = 0, length = string.length(); i < length; i++) {
|
||||||
out.append('\u0336').append(string.charAt(i));
|
out.append(string.charAt(i)).append('\u0336');
|
||||||
}
|
}
|
||||||
out.append('\u0335');
|
|
||||||
return out.toString();
|
return out.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -507,6 +507,8 @@ class PizzaBotTest {
|
|||||||
|
|
||||||
%s\{user.username()}: %.2f\{1.5 * margherita.price() + salami.price()} €
|
%s\{user.username()}: %.2f\{1.5 * margherita.price() + salami.price()} €
|
||||||
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} €
|
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} €
|
||||||
|
|
||||||
|
Gesamt: %.2f\{3 * margherita.price() + salami.price()} €
|
||||||
""".trim();
|
""".trim();
|
||||||
assertEquals(expected, strip(message.message()));
|
assertEquals(expected, strip(message.message()));
|
||||||
}
|
}
|
||||||
@@ -523,6 +525,8 @@ class PizzaBotTest {
|
|||||||
|
|
||||||
%s\{user.username()}: %.2f\{1.5 * margherita.price() + salami.price()} € (25.19 €)
|
%s\{user.username()}: %.2f\{1.5 * margherita.price() + salami.price()} € (25.19 €)
|
||||||
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} € (14.81 €)
|
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} € (14.81 €)
|
||||||
|
|
||||||
|
Gesamt: %.2f\{3 * margherita.price() + salami.price()} € (40.00 €)
|
||||||
""".trim();
|
""".trim();
|
||||||
assertEquals(expected, strip(message.message()));
|
assertEquals(expected, strip(message.message()));
|
||||||
}
|
}
|
||||||
@@ -539,6 +543,8 @@ class PizzaBotTest {
|
|||||||
|
|
||||||
%s\{user.username()}: %.2f\{1.5 * margherita.price() + salami.price()} €
|
%s\{user.username()}: %.2f\{1.5 * margherita.price() + salami.price()} €
|
||||||
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} €
|
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} €
|
||||||
|
|
||||||
|
Gesamt: %.2f\{3 * margherita.price() + salami.price()} € %.2f\{1.5 * margherita.price()} €
|
||||||
""".trim();
|
""".trim();
|
||||||
assertEquals(expected, strip(message.message()));
|
assertEquals(expected, strip(message.message()));
|
||||||
assertEquals(strikethrough("User1: " + tabular(1.5 * margherita.price() + salami.price(), 0) + " €"), message.message().split("\n")[3]);
|
assertEquals(strikethrough("User1: " + tabular(1.5 * margherita.price() + salami.price(), 0) + " €"), message.message().split("\n")[3]);
|
||||||
@@ -561,6 +567,8 @@ class PizzaBotTest {
|
|||||||
|
|
||||||
%s\{user.username()}: %.2f\{user1Total} € %.2f\{user1Remaining} €
|
%s\{user.username()}: %.2f\{user1Total} € %.2f\{user1Remaining} €
|
||||||
%s\{user2.username()}: %.2f\{user2Total} € %.2f\{user2Remaining} €
|
%s\{user2.username()}: %.2f\{user2Total} € %.2f\{user2Remaining} €
|
||||||
|
|
||||||
|
Gesamt: %.2f\{user1Total + user2Total} € %.2f\{user1Remaining + user2Remaining} €
|
||||||
""".trim();
|
""".trim();
|
||||||
assertEquals(expected, strip(message.message()));
|
assertEquals(expected, strip(message.message()));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@@ -585,6 +593,8 @@ class PizzaBotTest {
|
|||||||
|
|
||||||
%s\{user.username()}: %.2f\{2.5 * margherita.price() + salami.price()} € (115.19 €)
|
%s\{user.username()}: %.2f\{2.5 * margherita.price() + salami.price()} € (115.19 €)
|
||||||
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} € (14.81 €)
|
%s\{user2.username()}: %.2f\{1.5 * margherita.price()} € (14.81 €)
|
||||||
|
|
||||||
|
Gesamt: %.2f\{4 * margherita.price() + salami.price()} € (130.00 €)
|
||||||
""".trim();
|
""".trim();
|
||||||
assertEquals(expected, strip(message.message()));
|
assertEquals(expected, strip(message.message()));
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("chat-bot.bot-conventions")
|
|
||||||
}
|
|
||||||
|
|
||||||
group = "eu.jonahbauer.chat.bots"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":bot-database"))
|
|
||||||
testImplementation(testFixtures(project(":bot-api")))
|
|
||||||
}
|
|
@@ -1,289 +0,0 @@
|
|||||||
package eu.jonahbauer.chat.bot.sleep;
|
|
||||||
|
|
||||||
import eu.jonahbauer.chat.bot.api.ChatBot;
|
|
||||||
import eu.jonahbauer.chat.bot.api.Message.Post;
|
|
||||||
import eu.jonahbauer.chat.bot.config.BotConfig;
|
|
||||||
import eu.jonahbauer.chat.bot.database.Database;
|
|
||||||
import eu.jonahbauer.chat.bot.sleep.model.EventType;
|
|
||||||
import eu.jonahbauer.chat.bot.sleep.model.SleepEvent;
|
|
||||||
import lombok.Lombok;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class SleepBot extends ChatBot {
|
|
||||||
private static final BotConfig.Key.OfString DATABASE = BotConfig.Key.ofString("database");
|
|
||||||
|
|
||||||
// name matching
|
|
||||||
private static final BotConfig.Key.OfStringArray WHITELIST = BotConfig.Key.ofStringArray("whitelist");
|
|
||||||
private static final BotConfig.Key.OfStringArray BLACKLIST = BotConfig.Key.ofStringArray("blacklist");
|
|
||||||
private static final BotConfig.Key.OfBoolean WHITESPACE_SENSITIVE = BotConfig.Key.ofBoolean("whitespace_sensitive");
|
|
||||||
|
|
||||||
// type matching
|
|
||||||
private static final BotConfig.Key.OfStringArray MINK_KEYWORDS = BotConfig.Key.ofStringArray("mink");
|
|
||||||
private static final BotConfig.Key.OfStringArray ANKH_KEYWORDS = BotConfig.Key.ofStringArray("ankh");
|
|
||||||
private static final BotConfig.Key.OfLong MAX_MESSAGE_LENGTH = BotConfig.Key.ofLong("max_message_length");
|
|
||||||
private static final BotConfig.Key.OfBoolean CASE_SENSITIVE = BotConfig.Key.ofBoolean("case_sensitive");
|
|
||||||
|
|
||||||
// duration
|
|
||||||
private static final BotConfig.Key.OfLong MIN_SLEEP_SECONDS = BotConfig.Key.ofLong("min_sleep_seconds");
|
|
||||||
private static final BotConfig.Key.OfLong MAX_SLEEP_SECONDS = BotConfig.Key.ofLong("max_sleep_seconds");
|
|
||||||
|
|
||||||
private static final BotConfig DEFAULT_CONFIG = BotConfig.builder()
|
|
||||||
.name("Schlaf-Tracker")
|
|
||||||
.value(WHITESPACE_SENSITIVE, false)
|
|
||||||
.value(CASE_SENSITIVE, false)
|
|
||||||
.value(MINK_KEYWORDS, List.of("mink"))
|
|
||||||
.value(ANKH_KEYWORDS, List.of("ankh"))
|
|
||||||
.value(MAX_MESSAGE_LENGTH, 100L)
|
|
||||||
.value(MIN_SLEEP_SECONDS, 3600L)
|
|
||||||
.value(MAX_SLEEP_SECONDS, 16 * 3600L)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private final @NotNull Database database;
|
|
||||||
|
|
||||||
// name matching
|
|
||||||
private final @Nullable Filter filter;
|
|
||||||
private final boolean whitespaceSensitive;
|
|
||||||
|
|
||||||
// type matching
|
|
||||||
private final @NotNull Set<String> mink;
|
|
||||||
private final @NotNull Set<String> ankh;
|
|
||||||
private final boolean caseSensitive;
|
|
||||||
private final long maxMessageLength;
|
|
||||||
|
|
||||||
// durations
|
|
||||||
private final @NotNull Duration minSleepDuration;
|
|
||||||
private final @NotNull Duration maxSleepDuration;
|
|
||||||
|
|
||||||
public SleepBot() {
|
|
||||||
super(DEFAULT_CONFIG);
|
|
||||||
|
|
||||||
this.database = new Database(getConfig().require(DATABASE));
|
|
||||||
|
|
||||||
var whitelist = getConfig().get(WHITELIST);
|
|
||||||
var blacklist = getConfig().get(BLACKLIST);
|
|
||||||
|
|
||||||
if (whitelist.isPresent()) {
|
|
||||||
var items = new ArrayList<>(whitelist.get());
|
|
||||||
blacklist.ifPresent(items::removeAll);
|
|
||||||
this.filter = new Filter.WhiteList(items);
|
|
||||||
} else {
|
|
||||||
this.filter = blacklist.map(Filter.BlackList::new).orElse(null);
|
|
||||||
}
|
|
||||||
this.whitespaceSensitive = getConfig().require(WHITESPACE_SENSITIVE);
|
|
||||||
|
|
||||||
this.caseSensitive = getConfig().require(CASE_SENSITIVE);
|
|
||||||
this.maxMessageLength = getConfig().require(MAX_MESSAGE_LENGTH);
|
|
||||||
var normalize = caseSensitive
|
|
||||||
? (Function<List<String>, Set<String>>) Set::copyOf
|
|
||||||
: (Function<List<String>, Set<String>>) items -> Set.of(items.stream().map(str -> str.toLowerCase(Locale.ROOT)).toArray(String[]::new));
|
|
||||||
this.mink = normalize.apply(getConfig().require(MINK_KEYWORDS));
|
|
||||||
this.ankh = normalize.apply(getConfig().require(ANKH_KEYWORDS));
|
|
||||||
|
|
||||||
this.minSleepDuration = Duration.ofSeconds(getConfig().require(MIN_SLEEP_SECONDS));
|
|
||||||
this.maxSleepDuration = Duration.ofSeconds(getConfig().require(MAX_SLEEP_SECONDS));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMessage(@NotNull Post post) {
|
|
||||||
if (!isTracked(post)) return;
|
|
||||||
|
|
||||||
var type = getEventType(post);
|
|
||||||
if (type.isEmpty()) return;
|
|
||||||
|
|
||||||
var event = new SleepEvent(getUserName(post), type.get(), Instant.now());
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (type.get() == EventType.MINK) handleMink(post, event);
|
|
||||||
database.save(event);
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
throw Lombok.sneakyThrow(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleMink(@NotNull Post post, @NotNull SleepEvent mink) throws SQLException {
|
|
||||||
var events = getSleepEvents(getUserName(post));
|
|
||||||
var durations = getSleepDurations(events);
|
|
||||||
if (durations.isEmpty()) return;
|
|
||||||
|
|
||||||
var previous = events.getLast();
|
|
||||||
if (previous.type() != EventType.ANKH) {
|
|
||||||
post("Hat da jemand ein \"Ankh\" vergessen?");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var duration = Duration.between(previous.timestamp(), mink.timestamp());
|
|
||||||
if (duration.compareTo(minSleepDuration) < 0 || duration.compareTo(maxSleepDuration) > 0) return;
|
|
||||||
|
|
||||||
var statistics = Statistics.of(durations);
|
|
||||||
log.debug("Sleep-Statistics for \"{}\": {}", getUserName(post), statistics);
|
|
||||||
|
|
||||||
if (duration.compareTo(statistics.longest()) > 0) {
|
|
||||||
post(STR."""
|
|
||||||
Guten Morgen \{post.name().trim()}! Du hast \{toString(duration)} geschlafen. \
|
|
||||||
So lange hast du seit noch nie geschlafen.\
|
|
||||||
""");
|
|
||||||
} else if (duration.compareTo(statistics.shortest()) < 0) {
|
|
||||||
post(STR."""
|
|
||||||
Guten Morgen \{post.name().trim()}! Du hast \{toString(duration)} geschlafen. \
|
|
||||||
So kurz hast du noch nie geschlafen.\
|
|
||||||
""");
|
|
||||||
} else if (duration.compareTo(statistics.average().plus(statistics.deviation())) > 0) {
|
|
||||||
post(STR."""
|
|
||||||
Guten Morgen \{post.name().trim()}! Du hast überdurchschnittliche \{toString(duration)} geschlafen. \
|
|
||||||
""");
|
|
||||||
} else if (duration.compareTo(statistics.average().minus(statistics.deviation())) < 0) {
|
|
||||||
post(STR."""
|
|
||||||
Guten Morgen \{post.name().trim()}! Du hast unterdurchschnittliche \{toString(duration)} geschlafen. \
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull String toString(@NotNull Duration duration) {
|
|
||||||
var seconds = duration.getSeconds();
|
|
||||||
var hours = seconds / 3600;
|
|
||||||
var minutes = seconds / 60 % 60;
|
|
||||||
return STR."\{hours} Stunden und \{minutes} Minuten";
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTracked(@NotNull Post post) {
|
|
||||||
return filter == null || filter.test(getUserName(post));
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull String getUserName(@NotNull Post post) {
|
|
||||||
var name = post.name();
|
|
||||||
if (!whitespaceSensitive) name = name.trim();
|
|
||||||
if (!caseSensitive) name = name.toLowerCase(Locale.ROOT);
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull Optional<EventType> getEventType(@NotNull Post post) {
|
|
||||||
var message = post.message();
|
|
||||||
if (message.length() > maxMessageLength) return Optional.empty();
|
|
||||||
if (!caseSensitive) message = message.toLowerCase(Locale.ROOT);
|
|
||||||
|
|
||||||
for (var msg : mink) if (message.contains(msg)) return Optional.of(EventType.MINK);
|
|
||||||
for (var msg : ankh) if (message.contains(msg)) return Optional.of(EventType.ANKH);
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull List<SleepEvent> getSleepEvents(@NotNull String name) throws SQLException {
|
|
||||||
return database.executeQuery(
|
|
||||||
SleepEvent.class, "SELECT * FROM sleep_event WHERE user = ? ORDER BY timestamp",
|
|
||||||
stmt -> stmt.setString(1, name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull List<Duration> getSleepDurations(@NotNull List<@NotNull SleepEvent> events) {
|
|
||||||
return getSleepDurations(events, minSleepDuration, maxSleepDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NotNull List<Duration> getSleepDurations(@NotNull List<@NotNull SleepEvent> events, @NotNull Duration min, @NotNull Duration max) {
|
|
||||||
var durations = new ArrayList<Duration>();
|
|
||||||
|
|
||||||
var it = events.iterator();
|
|
||||||
while (true) {
|
|
||||||
var duration = next(it);
|
|
||||||
if (duration.isEmpty()) break;
|
|
||||||
|
|
||||||
if (min.compareTo(duration.get()) < 0 && duration.get().compareTo(max) < 0) {
|
|
||||||
durations.add(duration.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return durations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NotNull Optional<Duration> next(@NotNull Iterator<@NotNull SleepEvent> it) {
|
|
||||||
if (!it.hasNext()) return Optional.empty();
|
|
||||||
var previous = it.next();
|
|
||||||
|
|
||||||
while (it.hasNext()) {
|
|
||||||
var current = it.next();
|
|
||||||
if (previous.type() == EventType.ANKH && current.type() == EventType.MINK) {
|
|
||||||
return Optional.of(Duration.between(previous.timestamp(), current.timestamp()));
|
|
||||||
}
|
|
||||||
previous = current;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private record Statistics(
|
|
||||||
@NotNull Duration shortest,
|
|
||||||
@NotNull Duration longest,
|
|
||||||
@NotNull Duration average,
|
|
||||||
@NotNull Duration deviation,
|
|
||||||
int n
|
|
||||||
) {
|
|
||||||
private static @NotNull Statistics of(@NotNull List<@NotNull Duration> durations) {
|
|
||||||
if (durations.isEmpty()) throw new IllegalArgumentException();
|
|
||||||
|
|
||||||
var shortest = durations.getFirst();
|
|
||||||
var longest = durations.getFirst();
|
|
||||||
var sum = Duration.ZERO;
|
|
||||||
|
|
||||||
for (var duration : durations) {
|
|
||||||
if (duration.compareTo(shortest) < 0) shortest = duration;
|
|
||||||
if (duration.compareTo(longest) > 0) longest = duration;
|
|
||||||
sum = sum.plus(duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
var mean = sum.dividedBy(durations.size());
|
|
||||||
long deviation = 0;
|
|
||||||
|
|
||||||
for (Duration duration : durations) {
|
|
||||||
var delta = duration.getSeconds() - mean.getSeconds();
|
|
||||||
deviation = Math.addExact(deviation, Math.multiplyExact(delta, delta));
|
|
||||||
}
|
|
||||||
|
|
||||||
var standard = Duration.ofSeconds((long) Math.sqrt((double) deviation / durations.size()));
|
|
||||||
|
|
||||||
return new Statistics(shortest, longest, mean, standard, durations.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Statistics {
|
|
||||||
Objects.requireNonNull(shortest);
|
|
||||||
Objects.requireNonNull(longest);
|
|
||||||
Objects.requireNonNull(average);
|
|
||||||
Objects.requireNonNull(deviation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed interface Filter extends Predicate<@NotNull String> {
|
|
||||||
|
|
||||||
record BlackList(@NotNull List<@NotNull String> list) implements Filter {
|
|
||||||
|
|
||||||
public BlackList {
|
|
||||||
list = List.copyOf(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean test(@NotNull String name) {
|
|
||||||
return !list.contains(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record WhiteList(@NotNull List<@NotNull String> list) implements Filter {
|
|
||||||
|
|
||||||
public WhiteList {
|
|
||||||
list = List.copyOf(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean test(@NotNull String name) {
|
|
||||||
return list.contains(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
package eu.jonahbauer.chat.bot.sleep.model;
|
|
||||||
|
|
||||||
public enum EventType {
|
|
||||||
MINK, ANKH
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
package eu.jonahbauer.chat.bot.sleep.model;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public record SleepEvent(
|
|
||||||
@NotNull String user,
|
|
||||||
@NotNull EventType type,
|
|
||||||
@NotNull Instant timestamp
|
|
||||||
) implements Comparable<SleepEvent> {
|
|
||||||
public SleepEvent {
|
|
||||||
Objects.requireNonNull(user);
|
|
||||||
Objects.requireNonNull(type);
|
|
||||||
Objects.requireNonNull(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(@NotNull SleepEvent sleepEvent) {
|
|
||||||
return timestamp.compareTo(sleepEvent.timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
import eu.jonahbauer.chat.bot.api.ChatBot;
|
|
||||||
import eu.jonahbauer.chat.bot.sleep.SleepBot;
|
|
||||||
|
|
||||||
module eu.jonahbauer.chat.bot.sleep {
|
|
||||||
exports eu.jonahbauer.chat.bot.sleep.model to eu.jonahbauer.chat.bot.database;
|
|
||||||
|
|
||||||
requires eu.jonahbauer.chat.bot.api;
|
|
||||||
requires eu.jonahbauer.chat.bot.database;
|
|
||||||
|
|
||||||
requires org.slf4j;
|
|
||||||
requires static lombok;
|
|
||||||
|
|
||||||
provides ChatBot with SleepBot;
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE sleep_event(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user VARCHAR(255) NOT NULL,
|
|
||||||
type VARCHAR(16) NOT NULL CHECK (type IN ('MINK', 'ANKH')),
|
|
||||||
timestamp INTEGER NOT NULL
|
|
||||||
);
|
|
@@ -1 +1 @@
|
|||||||
version=0.2.0
|
version=0.2.0-SNAPSHOT
|
@@ -15,9 +15,6 @@ project(":bots:ping-bot").name = "ping-bot"
|
|||||||
include("bots:pizza-bot")
|
include("bots:pizza-bot")
|
||||||
project(":bots:pizza-bot").name = "pizza-bot"
|
project(":bots:pizza-bot").name = "pizza-bot"
|
||||||
|
|
||||||
include("bots:sleep-tracker")
|
|
||||||
project(":bots:sleep-tracker").name = "sleep-tracker"
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
Reference in New Issue
Block a user