Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚀 Add timespan details expression & Improvements #4661

Merged
merged 35 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
870e8ff
🚀 Add timespan details expression
AyhamAl-Ali Mar 12, 2022
5f7f92e
Merge branch 'master' into ench/timespan-improve
AyhamAl-Ali Mar 13, 2022
1669cfa
Changes
AyhamAl-Ali Mar 15, 2022
8fe9d0a
Improvements
AyhamAl-Ali Mar 18, 2022
9367856
Changes
AyhamAl-Ali Mar 20, 2022
cf00390
Add DD:HH:MM:SS.ms + improve toString
AyhamAl-Ali Mar 21, 2022
68a2096
Merge branch 'master' into ench/timespan-improve
AyhamAl-Ali Jun 24, 2022
8af2008
Update src/main/java/ch/njol/skript/util/Timespan.java
AyhamAl-Ali Jul 9, 2022
fdd2c9c
Update src/main/java/ch/njol/skript/util/Timespan.java
AyhamAl-Ali Jul 9, 2022
bf52019
Update src/main/java/ch/njol/skript/util/Timespan.java
AyhamAl-Ali Jul 9, 2022
3a4d6c3
Update src/main/java/ch/njol/skript/util/Timespan.java
AyhamAl-Ali Jul 9, 2022
a32ca2a
Testing
AyhamAl-Ali Jul 9, 2022
6372da2
Improvements - Unfinished code
AyhamAl-Ali Jul 16, 2022
0ec45dd
Update enums
AyhamAl-Ali Jan 1, 2023
9f4bbfb
Merge remote-tracking branch 'AyhamAl-Ali/ench/timespan-improve' into…
AyhamAl-Ali Jan 1, 2023
53ea288
Merge branch 'master' into ench/timespan-improve
AyhamAl-Ali Jan 1, 2023
207f2d7
Fix building
AyhamAl-Ali Jan 1, 2023
4745e8e
Merge remote-tracking branch 'AyhamAl-Ali/ench/timespan-improve' into…
AyhamAl-Ali Jan 1, 2023
56a77af
Test build
AyhamAl-Ali Jan 1, 2023
df3d9a9
Test 2
AyhamAl-Ali Jan 1, 2023
78e8b0e
Fix code
AyhamAl-Ali Jan 1, 2023
0ce18e9
Requested Changes & Improvements
AyhamAl-Ali Apr 18, 2023
6c5683d
Requested Changes
AyhamAl-Ali Apr 19, 2023
fcdb1b6
Merge branch 'master' into ench/timespan-improve
AyhamAl-Ali Apr 19, 2023
201b546
Pikachu's quality requested changes 🚀
AyhamAl-Ali Apr 20, 2023
37afcb9
Merge remote-tracking branch 'AyhamAl-Ali/ench/timespan-improve' into…
AyhamAl-Ali Apr 20, 2023
0283bea
Merge branch 'master' into ench/timespan-improve
TheLimeGlass Aug 5, 2023
2160bdb
Merge remote-tracking branch 'origin/dev/feature' into ench/timespan-…
AyhamAl-Ali Oct 5, 2023
63b281f
Apply suggestions
AyhamAl-Ali Oct 5, 2023
a2be2b1
typo
AyhamAl-Ali Oct 5, 2023
6be6300
Update src/main/java/ch/njol/skript/expressions/ExprTimespanDetails.java
AyhamAl-Ali Dec 15, 2023
2ac1b90
Merge branch 'dev/feature' into ench/timespan-improve
AyhamAl-Ali Dec 30, 2023
2b18a04
Add Timespan(TimePeriod, long) and deprecate fromTicks
AyhamAl-Ali Jan 21, 2024
1b845c7
Add getAs(TimePeriod)
AyhamAl-Ali Jan 21, 2024
001c9e3
Merge branch 'dev/feature' into ench/timespan-improve
sovdeeth Mar 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/main/java/ch/njol/skript/expressions/ExprTimespanDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* This file is part of Skript.
*
* Skript is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Skript 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Skript. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright Peter Güttinger, SkriptLang team and contributors
*/
package ch.njol.skript.expressions;

import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
import ch.njol.skript.doc.Since;
import ch.njol.skript.expressions.base.SimplePropertyExpression;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.util.Timespan;
import ch.njol.skript.util.Timespan.TimePeriod;
import ch.njol.util.Kleenean;
import org.eclipse.jdt.annotation.Nullable;

import java.util.Locale;

@Name("Timespan Details")
@Description("Retrieve specific information of a <a href=\"/classes.html#timespan\">timespan</a> such as hours/minutes/etc.")
@Examples({
"set {_t} to difference between now and {Payouts::players::%uuid of player%::last-date}",
"send \"It has been %days of {_t}% day(s) since last payout.\""
})
@Since("INSERT VERSION")
public class ExprTimespanDetails extends SimplePropertyExpression<Timespan, Long> {

static {
register(ExprTimespanDetails.class, Long.class, "(:(tick|second|minute|hour|day|week|month|year))[s]", "timespans");
AyhamAl-Ali marked this conversation as resolved.
Show resolved Hide resolved
}

@SuppressWarnings("NotNullFieldNotInitialized")
private TimePeriod type;

@Override
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
type = TimePeriod.valueOf(parseResult.tags.get(0).toUpperCase(Locale.ENGLISH));
return super.init(exprs, matchedPattern, isDelayed, parseResult);
}

@Override
@Nullable
public Long convert(Timespan time) {
return time.getMilliSeconds() / type.getTime();
}

@Override
public Class<? extends Long> getReturnType() {
return Long.class;
}

@Override
protected String getPropertyName() {
return type.name().toLowerCase(Locale.ENGLISH);
}

}
188 changes: 115 additions & 73 deletions src/main/java/ch/njol/skript/util/Timespan.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
*/
package ch.njol.skript.util;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Locale;

import org.eclipse.jdt.annotation.Nullable;
Expand All @@ -35,91 +40,133 @@

public class Timespan implements YggdrasilSerializable, Comparable<Timespan> { // REMIND unit

private static final Noun m_tick = new Noun("time.tick");
private static final Noun m_second = new Noun("time.second");
private static final Noun m_minute = new Noun("time.minute");
private static final Noun m_hour = new Noun("time.hour");
private static final Noun m_day = new Noun("time.day");
private static final Noun m_week = new Noun("time.week");
private static final Noun m_month = new Noun("time.month");
private static final Noun m_year = new Noun("time.year");
static final Noun[] names = {m_tick, m_second, m_minute, m_hour, m_day, m_week, m_month, m_year};
static final long[] times = {50L, 1000L, 1000L * 60L, 1000L * 60L * 60L, 1000L * 60L * 60L * 24L, 1000L * 60L * 60L * 24L * 7L, 1000L * 60L * 60L * 24L * 30L, 1000L * 60L * 60L * 24L * 365L};
static final HashMap<String, Long> parseValues = new HashMap<>();

public enum TimePeriod {

TICK(50L),
SECOND(1000L),
MINUTE(SECOND.time * 60L),
HOUR(MINUTE.time * 60L),
DAY(HOUR.time * 24L),
WEEK(DAY.time * 7L),
MONTH(DAY.time * 30L), // Who cares about 28, 29 or 31 days?
YEAR(DAY.time * 365L);

private final Noun name;
private final long time;

TimePeriod(long time) {
this.name = new Noun("time." + this.name().toLowerCase(Locale.ENGLISH));
this.time = time;
}

public long getTime() {
return time;
}

}

private static final List<NonNullPair<Noun, Long>> SIMPLE_VALUES = Arrays.asList(
new NonNullPair<>(TimePeriod.YEAR.name, TimePeriod.YEAR.time),
new NonNullPair<>(TimePeriod.MONTH.name, TimePeriod.MONTH.time),
new NonNullPair<>(TimePeriod.WEEK.name, TimePeriod.WEEK.time),
new NonNullPair<>(TimePeriod.DAY.name, TimePeriod.DAY.time),
new NonNullPair<>(TimePeriod.HOUR.name, TimePeriod.HOUR.time),
new NonNullPair<>(TimePeriod.MINUTE.name, TimePeriod.MINUTE.time),
new NonNullPair<>(TimePeriod.SECOND.name, TimePeriod.SECOND.time)
);

private static final Map<String, Long> PARSE_VALUES = new HashMap<>();

static {
Language.addListener(new LanguageChangeListener() {
@Override
public void onLanguageChange() {
for (int i = 0; i < names.length; i++) {
parseValues.put(names[i].getSingular().toLowerCase(Locale.ENGLISH), times[i]);
parseValues.put(names[i].getPlural().toLowerCase(Locale.ENGLISH), times[i]);
for (TimePeriod time : TimePeriod.values()) {
PARSE_VALUES.put(time.name.getSingular().toLowerCase(Locale.ENGLISH), time.getTime());
PARSE_VALUES.put(time.name.getPlural().toLowerCase(Locale.ENGLISH), time.getTime());
}
}
});
}

private static final Pattern TIMESPAN_PATTERN = Pattern.compile("^(\\d+):(\\d\\d)(:\\d\\d){0,2}(?<ms>\\.\\d{1,4})?$");
private static final Pattern TIMESPAN_NUMBER_PATTERN = Pattern.compile("^\\d+(\\.\\d+)?$");
private static final Pattern TIMESPAN_SPLIT_PATTERN = Pattern.compile("[:.]");

private final long millis;

@Nullable
public static Timespan parse(final String s) {
if (s.isEmpty())
public static Timespan parse(String value) {
if (value.isEmpty())
return null;

long t = 0;
boolean minecraftTime = false;
boolean isMinecraftTimeSet = false;
if (s.matches("^\\d+:\\d\\d(:\\d\\d)?(\\.\\d{1,4})?$")) { // MM:SS[.ms] or HH:MM:SS[.ms]
final String[] ss = s.split("[:.]");
final long[] times = {1000L * 60L * 60L, 1000L * 60L, 1000L, 1L}; // h, m, s, ms

final int offset = ss.length == 3 && !s.contains(".") || ss.length == 4 ? 0 : 1;
for (int i = 0; i < ss.length; i++) {
t += times[offset + i] * Utils.parseLong("" + ss[i]);

Matcher matcher = TIMESPAN_PATTERN.matcher(value);
if (matcher.matches()) { // MM:SS[.ms] or HH:MM:SS[.ms] or DD:HH:MM:SS[.ms]
String[] substring = TIMESPAN_SPLIT_PATTERN.split(value);
long[] times = {1L, TimePeriod.SECOND.time, TimePeriod.MINUTE.time, TimePeriod.HOUR.time, TimePeriod.DAY.time}; // ms, s, m, h, d
boolean hasMs = value.contains(".");
int length = substring.length;
int offset = 2; // MM:SS[.ms]

if (length == 4 && !hasMs || length == 5) // DD:HH:MM:SS[.ms]
offset = 0;
else if (length == 3 && !hasMs || length == 4) // HH:MM:SS[.ms]
offset = 1;

for (int i = 0; i < substring.length; i++) {
t += times[offset + i] * Utils.parseLong("" + substring[i]);
}
} else { // <number> minutes/seconds/.. etc
final String[] subs = s.toLowerCase(Locale.ENGLISH).split("\\s+");
for (int i = 0; i < subs.length; i++) {
String sub = subs[i];
String[] substring = value.toLowerCase(Locale.ENGLISH).split("\\s+");
for (int i = 0; i < substring.length; i++) {
String sub = substring[i];

if (sub.equals(GeneralWords.and.toString())) {
if (i == 0 || i == subs.length - 1)
if (i == 0 || i == substring.length - 1)
return null;
continue;
}

double amount = 1;
if (Noun.isIndefiniteArticle(sub)) {
if (i == subs.length - 1)
if (i == substring.length - 1)
return null;
amount = 1;
sub = subs[++i];
} else if (sub.matches("^\\d+(\\.\\d+)?$")) {
if (i == subs.length - 1)
sub = substring[++i];
} else if (TIMESPAN_NUMBER_PATTERN.matcher(sub).matches()) {
if (i == substring.length - 1)
return null;
try {
amount = Double.parseDouble(sub);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid timespan: " + s);
throw new IllegalArgumentException("Invalid timespan: " + value);
}
sub = subs[++i];
sub = substring[++i];
}

if (CollectionUtils.contains(Language.getList("time.real"), sub)) {
if (i == subs.length - 1 || isMinecraftTimeSet && minecraftTime)
if (i == substring.length - 1 || isMinecraftTimeSet && minecraftTime)
return null;
sub = subs[++i];
sub = substring[++i];
} else if (CollectionUtils.contains(Language.getList("time.minecraft"), sub)) {
if (i == subs.length - 1 || isMinecraftTimeSet && !minecraftTime)
if (i == substring.length - 1 || isMinecraftTimeSet && !minecraftTime)
return null;
minecraftTime = true;
sub = subs[++i];
sub = substring[++i];
}

if (sub.endsWith(","))
sub = sub.substring(0, sub.length() - 1);

final Long d = parseValues.get(sub.toLowerCase(Locale.ENGLISH));
Long d = PARSE_VALUES.get(sub.toLowerCase(Locale.ENGLISH));
if (d == null)
return null;

if (minecraftTime && d != times[0]) // times[0] == tick
if (minecraftTime && d != TimePeriod.TICK.time)
amount /= 72f;

t += Math.round(amount * d);
Expand All @@ -128,16 +175,15 @@ public static Timespan parse(final String s) {

}
}

return new Timespan(t);
}

private final long millis;

public Timespan() {
millis = 0;
}

public Timespan(final long millis) {
public Timespan(long millis) {
AyhamAl-Ali marked this conversation as resolved.
Show resolved Hide resolved
if (millis < 0)
throw new IllegalArgumentException("millis must be >= 0");
this.millis = millis;
Expand Down Expand Up @@ -187,66 +233,62 @@ public String toString() {
return toString(millis);
}

public String toString(final int flags) {
public String toString(int flags) {
return toString(millis, flags);
}

@SuppressWarnings("unchecked")
static final NonNullPair<Noun, Long>[] simpleValues = new NonNullPair[] {
new NonNullPair<>(m_day, 1000L * 60 * 60 * 24),
new NonNullPair<>(m_hour, 1000L * 60 * 60),
new NonNullPair<>(m_minute, 1000L * 60),
new NonNullPair<>(m_second, 1000L)
};

public static String toString(final long millis) {
public static String toString(long millis) {
return toString(millis, 0);
}

public static String toString(final long millis, final int flags) {
for (int i = 0; i < simpleValues.length - 1; i++) {
if (millis >= simpleValues[i].getSecond()) {
final double second = 1. * (millis % simpleValues[i].getSecond()) / simpleValues[i + 1].getSecond();
@SuppressWarnings("null")
public static String toString(long millis, int flags) {
for (int i = 0; i < SIMPLE_VALUES.size() - 1; i++) {
if (millis >= SIMPLE_VALUES.get(i).getSecond()) {
long remainder = millis % SIMPLE_VALUES.get(i).getSecond();
double second = 1. * remainder / SIMPLE_VALUES.get(i + 1).getSecond();
if (!"0".equals(Skript.toString(second))) { // bad style but who cares...
return toString(Math.floor(1. * millis / simpleValues[i].getSecond()), simpleValues[i], flags) + " " + GeneralWords.and + " " + toString(second, simpleValues[i + 1], flags);
return toString(Math.floor(1. * millis / SIMPLE_VALUES.get(i).getSecond()), SIMPLE_VALUES.get(i), flags) + " " + GeneralWords.and + " " + toString(remainder, flags);
} else {
return toString(1. * millis / simpleValues[i].getSecond(), simpleValues[i], flags);
return toString(1. * millis / SIMPLE_VALUES.get(i).getSecond(), SIMPLE_VALUES.get(i), flags);
}
}
}
return toString(1. * millis / simpleValues[simpleValues.length - 1].getSecond(), simpleValues[simpleValues.length - 1], flags);
return toString(1. * millis / SIMPLE_VALUES.get(SIMPLE_VALUES.size() - 1).getSecond(), SIMPLE_VALUES.get(SIMPLE_VALUES.size() - 1), flags);
}

private static String toString(final double amount, final NonNullPair<Noun, Long> p, final int flags) {
return p.getFirst().withAmount(amount, flags);
private static String toString(double amount, NonNullPair<Noun, Long> pair, int flags) {
return pair.getFirst().withAmount(amount, flags);
}


/**
* Compare this Timespan with another
* @param time the Timespan to be compared.
* @return -1 if this Timespan is less than argument Timespan, 0 if equals and 1 if greater than
*/
@Override
public int compareTo(final @Nullable Timespan o) {
final long d = o == null ? millis : millis - o.millis;
return d > 0 ? 1 : d < 0 ? -1 : 0;
public int compareTo(@Nullable Timespan time) {
return Long.compare(millis, time == null ? millis : time.millis);
}

@Override
public int hashCode() {
final int prime = 31;
int prime = 31;
int result = 1;
result = prime * result + (int) (millis/Integer.MAX_VALUE);
result = prime * result + (int) (millis / Integer.MAX_VALUE);
return result;
}

@Override
public boolean equals(final @Nullable Object obj) {
public boolean equals(@Nullable Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Timespan))
return false;
final Timespan other = (Timespan) obj;
if (millis != other.millis)
return false;
return true;

return millis == ((Timespan) obj).millis;
}

}