/* * This file is part of Player Analytics (Plan). * * Plan is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License v3 as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Plan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Plan. If not, see . */ package com.djrapitops.plan.delivery.domain.mutators; import com.djrapitops.plan.delivery.domain.container.DataContainer; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.gathering.domain.FinishedSession; import com.djrapitops.plan.settings.locale.Locale; import com.djrapitops.plan.settings.locale.lang.HtmlLang; import java.util.List; import java.util.concurrent.TimeUnit; /** * Represents Activity index of a player at a certain date. *

* Old formula for activity index was not linear and difficult to turn into a query due to conditional multipliers. * Thus a new formula was written. *

* {@code T} - Time played after someone is considered active on a particular week * {@code t1, t2, t3} - Time played that week *

* Activity index takes into account last 3 weeks. *

* Activity for a single week is calculated using {@code A(t) = (1 / (pi/2 * (t/T) + 1))}. * A(t) is based on function f(x) = 1 / (x + 1), which has property f(0) = 1, decreasing from there, but not in a straight line. * You can see the function plotted here *

* To fine tune the curve pi/2 is used since it felt like a good curve. *

* Activity index A is calculated by using the formula: * {@code A = 5 - 5 * [A(t1) + A(t2) + A(t3)] / 3} *

* * Plot for A and limits * *

* New Limits for A would thus be * {@code < 1: Inactive} * {@code > 1: Irregular} * {@code > 2: Regular} * {@code > 3: Active} * {@code > 3.75: Very Active} */ public class ActivityIndex { public static final double VERY_ACTIVE = 3.75; public static final double ACTIVE = 3.0; public static final double REGULAR = 2.0; public static final double IRREGULAR = 1.0; private final double value; private final long date; private long playtimeMsThreshold; public ActivityIndex(DataContainer container, long date, long playtimeMsThreshold) { this.playtimeMsThreshold = playtimeMsThreshold; this.date = date; value = calculate(container); } public ActivityIndex(List sessions, long date, long playtimeMsThreshold) { this.playtimeMsThreshold = playtimeMsThreshold; this.date = date; value = calculate(new SessionsMutator(sessions)); } public ActivityIndex(double value, long date) { this.value = value; this.date = date; } public static String[] getDefaultGroups() { return getGroups(null); } public static String[] getDefaultGroupLangKeys() { return new String[]{ HtmlLang.INDEX_VERY_ACTIVE.getKey(), HtmlLang.INDEX_ACTIVE.getKey(), HtmlLang.INDEX_REGULAR.getKey(), HtmlLang.INDEX_IRREGULAR.getKey(), HtmlLang.INDEX_INACTIVE.getKey() }; } public static String[] getGroups(Locale locale) { if (locale == null) { return new String[]{ HtmlLang.INDEX_VERY_ACTIVE.getDefault(), HtmlLang.INDEX_ACTIVE.getDefault(), HtmlLang.INDEX_REGULAR.getDefault(), HtmlLang.INDEX_IRREGULAR.getDefault(), HtmlLang.INDEX_INACTIVE.getDefault() }; } return new String[]{ locale.getString(HtmlLang.INDEX_VERY_ACTIVE), locale.getString(HtmlLang.INDEX_ACTIVE), locale.getString(HtmlLang.INDEX_REGULAR), locale.getString(HtmlLang.INDEX_IRREGULAR), locale.getString(HtmlLang.INDEX_INACTIVE) }; } private double calculate(DataContainer container) { return calculate(SessionsMutator.forContainer(container)); } private double calculate(SessionsMutator sessionsMutator) { if (sessionsMutator.all().isEmpty()) { return 0.0; } long week = TimeUnit.DAYS.toMillis(7L); long weekAgo = date - week; long twoWeeksAgo = date - 2L * week; long threeWeeksAgo = date - 3L * week; SessionsMutator weekOne = sessionsMutator.filterSessionsBetween(weekAgo, date); SessionsMutator weekTwo = sessionsMutator.filterSessionsBetween(twoWeeksAgo, weekAgo); SessionsMutator weekThree = sessionsMutator.filterSessionsBetween(threeWeeksAgo, twoWeeksAgo); double playtime1 = weekOne.toActivePlaytime(); double playtime2 = weekTwo.toActivePlaytime(); double playtime3 = weekThree.toActivePlaytime(); double indexW1 = 1.0 / (Math.PI / 2.0 * (playtime1 / playtimeMsThreshold) + 1.0); double indexW2 = 1.0 / (Math.PI / 2.0 * (playtime2 / playtimeMsThreshold) + 1.0); double indexW3 = 1.0 / (Math.PI / 2.0 * (playtime3 / playtimeMsThreshold) + 1.0); double average = (indexW1 + indexW2 + indexW3) / 3.0; return 5.0 - (5.0 * average); } public double getValue() { return value; } public long getDate() { return date; } public String getFormattedValue(Formatter formatter) { return formatter.apply(value); } public double distance(ActivityIndex other) { // Logarithm makes the distance function more skewed towards active players // https://www.wolframalpha.com/input/?i=plot+y+%3D+log(5+-+5+*+(1+%2F+(pi%2F2+*+x%2B1)))+and+5+-+5+*+(1+%2F+(pi%2F2+*+x%2B1))+and+y+%3D1+and+y+%3D+2+and+y+%3D+3+and+y+%3D+3.75+from+-0.5+to+3 return Math.abs(Math.log(other.value) - Math.log(value)); } public static HtmlLang getGroupLang(double value) { if (value >= VERY_ACTIVE) { return HtmlLang.INDEX_VERY_ACTIVE; } else if (value >= ACTIVE) { return HtmlLang.INDEX_ACTIVE; } else if (value >= REGULAR) { return HtmlLang.INDEX_REGULAR; } else if (value >= IRREGULAR) { return HtmlLang.INDEX_IRREGULAR; } else { return HtmlLang.INDEX_INACTIVE; } } public static String getGroup(double value) { return getGroupLang(value).getDefault(); } public String getGroup() { return getGroup(value); } public String getGroupLang() { return getGroupLang(value).getKey(); } public String getGroup(Locale locale) { if (value >= VERY_ACTIVE) { return locale.getString(HtmlLang.INDEX_VERY_ACTIVE); } else if (value >= ACTIVE) { return locale.getString(HtmlLang.INDEX_ACTIVE); } else if (value >= REGULAR) { return locale.getString(HtmlLang.INDEX_REGULAR); } else if (value >= IRREGULAR) { return locale.getString(HtmlLang.INDEX_IRREGULAR); } else { return locale.getString(HtmlLang.INDEX_INACTIVE); } } }