pandalib-util javadoc and small API changes

This commit is contained in:
Marc Baloup 2022-07-28 01:13:35 +02:00
parent 5ff9a40f19
commit f7dc774dbd
Signed by: marcbal
GPG Key ID: BBC0FE3ABC30B893
27 changed files with 1585 additions and 1841 deletions

View File

@ -190,12 +190,12 @@ public interface SuggestionsSupplier<S> {
static <S> SuggestionsSupplier<S> suggestDuration() { static <S> SuggestionsSupplier<S> suggestDuration() {
final List<String> emptyTokenSuggestions = TimeUtil.DURATION_SUFFIXES.stream().map(p -> "1" + p).collect(Collectors.toList()); final List<String> emptyTokenSuggestions = DURATION_SUFFIXES.stream().map(p -> "1" + p).collect(Collectors.toList());
return (s, ti, token, args) -> { return (s, ti, token, args) -> {
if (token.isEmpty()) { if (token.isEmpty()) {
return emptyTokenSuggestions; return emptyTokenSuggestions;
} }
List<String> remainingSuffixes = new ArrayList<>(TimeUtil.DURATION_SUFFIXES); List<String> remainingSuffixes = new ArrayList<>(DURATION_SUFFIXES);
char[] tokenChars = token.toCharArray(); char[] tokenChars = token.toCharArray();
String accSuffix = ""; String accSuffix = "";
for (char c : tokenChars) { for (char c : tokenChars) {
@ -215,6 +215,11 @@ public interface SuggestionsSupplier<S> {
}; };
} }
/**
* List of all possible duration unit symbols for suggestions.
*/
public static final List<String> DURATION_SUFFIXES = List.of("y", "mo", "w", "d", "h", "m", "s");
private static void scanAndRemovePastSuffixes(List<String> suffixes, String foundSuffix) { private static void scanAndRemovePastSuffixes(List<String> suffixes, String foundSuffix) {
for (int i = 0; i < suffixes.size(); i++) { for (int i = 0; i < suffixes.size(); i++) {

View File

@ -88,9 +88,9 @@ public class AABBBlock implements Iterable<BlockVector> {
} }
public Vector getRandomPosition() { public Vector getRandomPosition() {
double x = RandomUtil.nextDoubleBetween(pos1.getX(), pos2.getX()); double x = RandomUtil.rand.nextDouble(pos1.getX(), pos2.getX());
double y = RandomUtil.nextDoubleBetween(pos1.getY(), pos2.getY()); double y = RandomUtil.rand.nextDouble(pos1.getY(), pos2.getY());
double z = RandomUtil.nextDoubleBetween(pos1.getZ(), pos2.getZ()); double z = RandomUtil.rand.nextDouble(pos1.getZ(), pos2.getZ());
return new Vector(x, y, z); return new Vector(x, y, z);
} }

View File

@ -73,7 +73,7 @@ public class GameWorldUtils implements Listener {
if (!new File(Bukkit.getWorldContainer(), world).isDirectory()) if (!new File(Bukkit.getWorldContainer(), world).isDirectory())
throw new IllegalStateException("GameWorld '"+world+"' does not exist"); throw new IllegalStateException("GameWorld '"+world+"' does not exist");
String copiedName = world + "_gen" + RandomUtil.nextIntBetween(100000, 999999); String copiedName = world + "_gen" + RandomUtil.rand.nextInt(100000, 999999);
File srcDir = new File(Bukkit.getWorldContainer(), world); File srcDir = new File(Bukkit.getWorldContainer(), world);
File destDir = new File(Bukkit.getWorldContainer(), copiedName); File destDir = new File(Bukkit.getWorldContainer(), copiedName);

View File

@ -65,9 +65,9 @@ public class LocationUtil {
// generate a random (x,z) coordinate // generate a random (x,z) coordinate
Location ret = new Location(w, Location ret = new Location(w,
RandomUtil.nextIntBetween(min.getBlockX(), max.getBlockX()) + 0.5, RandomUtil.rand.nextInt(min.getBlockX(), max.getBlockX()) + 0.5,
w.getMaxHeight() - 1, w.getMaxHeight() - 1,
RandomUtil.nextIntBetween(min.getBlockZ(), max.getBlockZ()) + 0.5); RandomUtil.rand.nextInt(min.getBlockZ(), max.getBlockZ()) + 0.5);
// find a secure y value // find a secure y value
ret = getSecureLocationOrNull(ret); ret = getSecureLocationOrNull(ret);

View File

@ -4,15 +4,82 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Utility class to track and limit the amount of a specific value for a specified amount of duration.
*
* An exemple of application is for rolling expense limit of a debit card: you cannot expense more that {@code $X}
* during a rolling period of {@code $Y} time.
*
* Here is an example usage of this class:
* <pre>
* AmountPerTimeLimiter instance = new AmountPerTimeLimiter(X, Y);
* void tryExpense(double amount) {
* if (instance.canAdd(amount)) {
* // do the action (here, its the expense)
* instance.add(amount);
* }
* }
* </pre>
*/
public class AmountPerTimeLimiter { public class AmountPerTimeLimiter {
private final double maxAmount; private final double maxAmount;
private final long duration; private final long duration;
private List<Entry> entries = new ArrayList<>(); private List<Entry> entries = new ArrayList<>();
public AmountPerTimeLimiter(double a, long d) { /**
maxAmount = a; * Create a new instance of {@link AmountPerTimeLimiter}
duration = d; * @param maximumAmount the maximum amount possible in the specified interval
* @param timeInterval the interval in milliseconds
*/
public AmountPerTimeLimiter(double maximumAmount, long timeInterval) {
maxAmount = maximumAmount;
duration = timeInterval;
}
/**
* Compute and returns the amount considered for the current time interval.
* @return the amount considered.
*/
public synchronized double getAmountSinceDuration() {
long currT = System.currentTimeMillis();
entries = entries.stream()
.filter(e -> e.time > currT - duration)
.collect(Collectors.toList());
return entries.stream()
.mapToDouble(e -> e.amount)
.sum();
}
/**
* Register the provided amount into this limiter, at the current system time.
* @param amount the amount added.
*/
public synchronized void add(double amount) {
long currT = System.currentTimeMillis();
if (!entries.isEmpty() && entries.get(entries.size()-1).time == currT)
entries.get(entries.size()-1).amount += amount;
else
entries.add(new Entry(currT, amount));
}
/**
* Determine if the provided amount can be added into the limiter without exceeding the limit.
* @param amount the amount to test.
* @return if its possible to add that amount now.
*/
public boolean canAdd(double amount) {
return getAmountSinceDuration() + amount < maxAmount;
}
/**
* Gets the amount that can be added right now into the limiter.
* @return the maximum amount that can be added.
*/
public double getRemainingForNow() {
return Math.max(0, maxAmount - getAmountSinceDuration());
} }
@ -26,32 +93,4 @@ public class AmountPerTimeLimiter {
time = t; amount = a; time = t; amount = a;
} }
} }
public synchronized double getAmountSinceDuration() {
long currT = System.currentTimeMillis();
entries = entries.stream()
.filter(e -> e.time > currT - duration)
.collect(Collectors.toList());
return entries.stream()
.mapToDouble(e -> e.amount)
.sum();
}
public synchronized void add(double amount) {
long currT = System.currentTimeMillis();
if (!entries.isEmpty() && entries.get(entries.size()-1).time > currT - 1000)
entries.get(entries.size()-1).amount += amount;
else
entries.add(new Entry(System.currentTimeMillis(), amount));
}
public boolean canAdd(double amount) {
return getAmountSinceDuration() + amount < maxAmount;
}
public double getRemainingForNow() {
return Math.max(0, maxAmount - getAmountSinceDuration());
}
} }

View File

@ -9,23 +9,52 @@ import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Supplier; import java.util.function.Supplier;
/**
* A bi-direction map storing in a synchronized way a {@code forwardMap} that store the key to value mapping, and a
* {@code backwardMap} that store the value to key mapping.
* All the keys and value are always unique in this bi-directional map.
* @param <K> the type of the "key"
* @param <V> the type of the "value"
*/
public class BiMap<K, V> implements Iterable<Entry<K, V>> { public class BiMap<K, V> implements Iterable<Entry<K, V>> {
protected final Map<K, V> map; /**
protected final Map<V, K> inversedMap; * The backend forward map
*/
protected final Map<K, V> forwardMap;
/**
* The backend bawkward map
*/
protected final Map<V, K> backwardMap;
private BiMap<V, K> reversedView = null; private BiMap<V, K> reversedView = null;
private final Object lock;
/**
* Create a new bi-directional map
* @param mapSupplier a {@link Supplier} to create the two backend map.
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public BiMap(Supplier<Map<?, ?>> mapSupplier) { public BiMap(Supplier<Map<?, ?>> mapSupplier) {
map = (Map<K, V>) mapSupplier.get(); forwardMap = (Map<K, V>) mapSupplier.get();
inversedMap = (Map<V, K>) mapSupplier.get(); backwardMap = (Map<V, K>) mapSupplier.get();
lock = new Object();
} }
/**
* Create a new bi-directional map with {@link HashMap} as the two backend maps.
*/
public BiMap() { public BiMap() {
this(HashMap::new); this(HashMap::new);
} }
/**
* Create a new bi-directional map with {@link HashMap} as the two backend maps, and filled with the provided source
* {@link Map}.
* @param source the source to fill the new {@link BiMap}.
*/
public BiMap(Map<K, V> source) { public BiMap(Map<K, V> source) {
this(); this();
putAll(source); putAll(source);
@ -35,90 +64,222 @@ public class BiMap<K, V> implements Iterable<Entry<K, V>> {
* Only used for #reversedView() * Only used for #reversedView()
*/ */
private BiMap(BiMap<V, K> rev) { private BiMap(BiMap<V, K> rev) {
map = rev.inversedMap; forwardMap = rev.backwardMap;
inversedMap = rev.map; backwardMap = rev.forwardMap;
lock = rev.lock;
reversedView = rev; reversedView = rev;
} }
public synchronized void put(K k, V v) { /**
* Associate the provided key and value to each other in this bi-directional map.
* Since the {@link BiMap} cannot have duplicate keys or values: if a key is already present, the currently mapped
* value will be removed from the map. Also if a value is already present, the currently mapped key will also be
* removed.
* @param k the key.
* @param v the value.
*/
public void put(K k, V v) {
synchronized (lock) {
if (containsKey(k)) if (containsKey(k))
remove(k); remove(k);
if (containsValue(v)) if (containsValue(v))
removeValue(v); removeValue(v);
map.put(k, v); forwardMap.put(k, v);
inversedMap.put(v, k); backwardMap.put(v, k);
}
public synchronized void putAll(Map<? extends K, ? extends V> source) {
for (Map.Entry<? extends K, ? extends V> e : source.entrySet()) {
put(e.getKey(), e.getValue());
} }
} }
public synchronized V get(K k) {
return map.get(k); /**
* Associate the provided key and value to each other in this bi-directional map.
* Since the {@link BiMap} cannot have duplicate keys or values: if a key is already present, the currently mapped
* value will be removed from the map. Also if a value is already present, the currently mapped key will also be
* removed.
* @param entry the entry with a key and value.
*/
public void put(Entry<? extends K, ? extends V> entry) {
put(entry.getKey(), entry.getValue());
} }
public synchronized K getKey(V v) { /**
return inversedMap.get(v); * Put the content of the provided map into this bi-directional map.
* This methods will call the {@link #put(Entry)} method successively in the order of the provided Maps iterator.
* @param source the source map.
*/
public void putAll(Map<? extends K, ? extends V> source) {
synchronized (lock) {
source.entrySet().forEach(this::put);
}
} }
public synchronized boolean containsKey(K k) { /**
return map.containsKey(k); * Gets the mapped value for the provided key.
* @param k the key.
* @return the value mapped with the key.
*/
public V get(K k) {
synchronized (lock) {
return forwardMap.get(k);
}
} }
public synchronized boolean containsValue(V v) { /**
return inversedMap.containsKey(v); * Gets the mapped key for the provided value.
* @param v the value.
* @return the key mapped with the value.
*/
public K getKey(V v) {
synchronized (lock) {
return backwardMap.get(v);
}
} }
public synchronized V remove(K k) { /**
V v = map.remove(k); * Tells if this {@link BiMap} contains the provided key.
inversedMap.remove(v); * @param k the key to test if its present.
* @return true if this bimap contains the provided key, false otherwise.
*/
public boolean containsKey(K k) {
synchronized (lock) {
return forwardMap.containsKey(k);
}
}
/**
* Tells if this {@link BiMap} contains the provided value.
* @param v the value to test if its present.
* @return true if this bimap contains the provided value, false otherwise.
*/
public boolean containsValue(V v) {
synchronized (lock) {
return backwardMap.containsKey(v);
}
}
/**
* Remove the mapping of the provided key from this map.
* The mapped value is also removed in the internal backward map.
* @param k the key whose mapping is to be removed from the map.
* @return the value that was mapped with the provided key.
*/
public V remove(K k) {
synchronized (lock) {
V v = forwardMap.remove(k);
backwardMap.remove(v);
return v; return v;
} }
}
public synchronized K removeValue(V v) { /**
K k = inversedMap.remove(v); * Remove the mapping of the provided value from this map.
map.remove(k); * The mapped key is also removed in the internal forward map.
* @param v the value whose mapping is to be removed from the map.
* @return the key that was mapped with the provided value.
*/
public K removeValue(V v) {
synchronized (lock) {
K k = backwardMap.remove(v);
forwardMap.remove(k);
return k; return k;
} }
}
/**
* Returns an unmodifiable {@link Set} view of this map.
* Its iteration order will depends on the implementation of the {@code forwardMap}.
* @return an unmodifiable {@link Set} view of this map.
* @see Map#entrySet()
*/
public Set<Entry<K, V>> entrySet() {
return Collections.unmodifiableSet(forwardMap.entrySet());
}
/**
* Returns an iterator of this map.
* Its iteration order will depends on the implementation of the {@code forwardMap}.
* @return an iterator of this map.
* @see Map#entrySet()
* @see Set#iterator()
*/
@Override @Override
public Iterator<Entry<K, V>> iterator() { public Iterator<Entry<K, V>> iterator() {
return Collections.unmodifiableSet(map.entrySet()).iterator(); return entrySet().iterator();
} }
/**
* Returns an unmodifiable {@link Set} view of the keys contained in this map.
* Its iteration order will depends on the implementation of the {@code forwardMap}s key set.
* @return an unmodifiable {@link Set} view of the keys contained in this map.
* @see Map#keySet()
*/
public Set<K> keySet() { public Set<K> keySet() {
return Collections.unmodifiableSet(map.keySet()); return Collections.unmodifiableSet(forwardMap.keySet());
} }
/**
* Returns an unmodifiable {@link Set} view of the values contained in this map.
* Its iteration order will depends on the implementation of the {@code backwardMap}s key set.
* @return an unmodifiable {@link Set} view of the values contained in this map.
* @see Map#keySet()
*/
public Set<V> valuesSet() { public Set<V> valuesSet() {
return Collections.unmodifiableSet(inversedMap.keySet()); return Collections.unmodifiableSet(backwardMap.keySet());
} }
/**
* Returns an unmodifiable {@link Map} view of the {@code forwardMap} of this bi-directional map.
* Its iteration order will depends on the implementation of the {@code forwardMap}.
* @return an unmodifiable {@link Map} view of the {@code forwardMap} of this bi-directional map.
*/
public Map<K, V> asMap() { public Map<K, V> asMap() {
return Collections.unmodifiableMap(map); return Collections.unmodifiableMap(forwardMap);
} }
/**
* Create a reversed view of this bi-directional map.
* Since the returned {@link BiMap} is a view of this {@link BiMap}, any change to either of those will affect both
* of them. Also, calling {@code bimap.reversedView().reversedView()} will return the original instance
* {@code bimap} since calling this method will cache each map into the respective reversed view.
* @return the reversed view of this {@link BiMap}.
*/
public BiMap<V, K> reversedView() { public BiMap<V, K> reversedView() {
synchronized (lock) {
if (reversedView == null) if (reversedView == null)
reversedView = new BiMap<>(this); reversedView = new BiMap<>(this);
return reversedView; return reversedView;
} }
}
public synchronized void forEach(BiConsumer<K, V> c) { /**
for(Entry<K, V> entry : this) { * Performs the provided action for each entry of this map, following the iteration order of the internal {@code forwardMap}.
c.accept(entry.getKey(), entry.getValue()); * @param action the action to perform on each entry.
* @see Map#forEach(BiConsumer)
*/
public void forEach(BiConsumer<K, V> action) {
synchronized (lock) {
forwardMap.forEach(action);
} }
} }
/**
* Returns the number of key-value mapping in this map.
* @return the number of key-value mapping in this map.
* @see Map#size()
*/
public int size() { public int size() {
return map.size(); synchronized (lock) {
return forwardMap.size();
}
} }
public synchronized void clear() { /**
map.clear(); * Removes all the mapping from this map.
inversedMap.clear(); */
public void clear() {
synchronized (lock) {
forwardMap.clear();
backwardMap.clear();
}
} }

View File

@ -1,629 +0,0 @@
package fr.pandacube.lib.util;
// from https://github.com/frode-carlsen/cron/blob/master/java8/src/main/java/fc/cron/CronExpression.java
// if there are changes, indicate them here (there is none currently)
/*
* Copyright (C) 2012- Frode Carlsen.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Note: rewritten to standard Java 8 DateTime by zemiak (c) 2016
*/
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This provides cron support for java8 using java-time.
* <P>
*
* Parser for unix-like cron expressions: Cron expressions allow specifying combinations of criteria for time
* such as: &quot;Each Monday-Friday at 08:00&quot; or &quot;Every last friday of the month at 01:30&quot;
* <p>
* A cron expressions consists of 5 or 6 mandatory fields (seconds may be omitted) separated by space. <br>
* These are:
*
* <table>
* <caption>CRON fields</caption>
* <tr>
* <th>Field</th>
* <th>Allowable values</th>
* <th>Special Characters</th>
* </tr>
* <tr>
* <td><code>Seconds (may be omitted)</code></td>
* <td><code>0-59</code></td>
* <td><code>, - * /</code></td>
* </tr>
* <tr>
* <td><code>Minutes</code></td>
* <td><code>0-59</code></td>
* <td><code>, - * /</code></td>
* </tr>
* <tr>
* <td><code>Hours</code></td>
* <td><code>0-23</code></td>
* <td><code>, - * /</code></td>
* </tr>
* <tr>
* <td><code>Day of month</code></td>
* <td><code>1-31</code></td>
* <td><code>, - * ? / L W</code></td>
* </tr>
* <tr>
* <td><code>Month</code></td>
* <td><code>1-12 or JAN-DEC (note: english abbreviations)</code></td>
* <td><code>, - * /</code></td>
* </tr>
* <tr>
* <td><code>Day of week</code></td>
* <td><code>1-7 or MON-SUN (note: english abbreviations)</code></td>
* <td><code>, - * ? / L #</code></td>
* </tr>
* </table>
*
* <P>
* '*' Can be used in all fields and means 'for all values'. E.g. &quot;*&quot; in minutes, means 'for all minutes'
* <P>
* '?' Can be used in Day-of-month and Day-of-week fields. Used to signify 'no special value'. It is used when one want
* to specify something for one of those two fields, but not the other.
* <P>
* '-' Used to specify a time interval. E.g. &quot;10-12&quot; in Hours field means 'for hours 10, 11 and 12'
* <P>
* ',' Used to specify multiple values for a field. E.g. &quot;MON,WED,FRI&quot; in Day-of-week field means &quot;for
* monday, wednesday and friday&quot;
* <P>
* '/' Used to specify increments. E.g. &quot;0/15&quot; in Seconds field means &quot;for seconds 0, 15, 30, ad
* 45&quot;. And &quot;5/15&quot; in seconds field means &quot;for seconds 5, 20, 35, and 50&quot;. If '*' s specified
* before '/' it is the same as saying it starts at 0. For every field there's a list of values that can be turned on or
* off. For Seconds and Minutes these range from 0-59. For Hours from 0 to 23, For Day-of-month it's 1 to 31, For Months
* 1 to 12. &quot;/&quot; character helsp turn some of these values back on. Thus &quot;7/6&quot; in Months field
* specify just Month 7. It doesn't turn on every 6 month following, since cron fields never roll over
* <P>
* 'L' Can be used on Day-of-month and Day-of-week fields. It signifies last day of the set of allowed values. In
* Day-of-month field it's the last day of the month (e.g.. 31 jan, 28 feb (29 in leap years), 31 march, etc.). In
* Day-of-week field it's Sunday. If there's a prefix, this will be subtracted (5L in Day-of-month means 5 days before
* last day of Month: 26 jan, 23 feb, etc.)
* <P>
* 'W' Can be specified in Day-of-Month field. It specifies closest weekday (monday-friday). Holidays are not accounted
* for. &quot;15W&quot; in Day-of-Month field means 'closest weekday to 15 i in given month'. If the 15th is a Saturday,
* it gives Friday. If 15th is a Sunday, the it gives following Monday.
* <P>
* '#' Can be used in Day-of-Week field. For example: &quot;5#3&quot; means 'third friday in month' (day 5 = friday, #3
* - the third). If the day does not exist (e.g. &quot;5#5&quot; - 5th friday of month) and there aren't 5 fridays in
* the month, then it won't match until the next month with 5 fridays.
* <P>
* <b>Case-sensitive</b> No fields are case-sensitive
* <P>
* <b>Dependencies between fields</b> Fields are always evaluated independently, but the expression doesn't match until
* the constraints of each field are met. Overlap of intervals are not allowed. That is: for
* Day-of-week field &quot;FRI-MON&quot; is invalid,but &quot;FRI-SUN,MON&quot; is valid
*
*/
public class CronExpression {
enum CronFieldType {
SECOND(0, 59, null) {
@Override
int getValue(ZonedDateTime dateTime) {
return dateTime.getSecond();
}
@Override
ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
return dateTime.withSecond(value).withNano(0);
}
@Override
ZonedDateTime overflow(ZonedDateTime dateTime) {
return dateTime.plusMinutes(1).withSecond(0).withNano(0);
}
},
MINUTE(0, 59, null) {
@Override
int getValue(ZonedDateTime dateTime) {
return dateTime.getMinute();
}
@Override
ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
return dateTime.withMinute(value).withSecond(0).withNano(0);
}
@Override
ZonedDateTime overflow(ZonedDateTime dateTime) {
return dateTime.plusHours(1).withMinute(0).withSecond(0).withNano(0);
}
},
HOUR(0, 23, null) {
@Override
int getValue(ZonedDateTime dateTime) {
return dateTime.getHour();
}
@Override
ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
return dateTime.withHour(value).withMinute(0).withSecond(0).withNano(0);
}
@Override
ZonedDateTime overflow(ZonedDateTime dateTime) {
return dateTime.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
}
},
DAY_OF_MONTH(1, 31, null) {
@Override
int getValue(ZonedDateTime dateTime) {
return dateTime.getDayOfMonth();
}
@Override
ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
return dateTime.withDayOfMonth(value).withHour(0).withMinute(0).withSecond(0).withNano(0);
}
@Override
ZonedDateTime overflow(ZonedDateTime dateTime) {
return dateTime.plusMonths(1).withDayOfMonth(0).withHour(0).withMinute(0).withSecond(0).withNano(0);
}
},
MONTH(1, 12,
Arrays.asList("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC")) {
@Override
int getValue(ZonedDateTime dateTime) {
return dateTime.getMonthValue();
}
@Override
ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
return dateTime.withMonth(value).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
}
@Override
ZonedDateTime overflow(ZonedDateTime dateTime) {
return dateTime.plusYears(1).withMonth(1).withHour(0).withDayOfMonth(1).withMinute(0).withSecond(0).withNano(0);
}
},
DAY_OF_WEEK(1, 7, Arrays.asList("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")) {
@Override
int getValue(ZonedDateTime dateTime) {
return dateTime.getDayOfWeek().getValue();
}
@Override
ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
throw new UnsupportedOperationException();
}
@Override
ZonedDateTime overflow(ZonedDateTime dateTime) {
throw new UnsupportedOperationException();
}
};
final int from, to;
final List<String> names;
CronFieldType(int from, int to, List<String> names) {
this.from = from;
this.to = to;
this.names = names;
}
/**
* @param dateTime {@link ZonedDateTime} instance
* @return The field time or date value from {@code dateTime}
*/
abstract int getValue(ZonedDateTime dateTime);
/**
* @param dateTime Initial {@link ZonedDateTime} instance to use
* @param value to set for this field in {@code dateTime}
* @return {@link ZonedDateTime} with {@code value} set for this field and all smaller fields cleared
*/
abstract ZonedDateTime setValue(ZonedDateTime dateTime, int value);
/**
* Handle when this field overflows and the next higher field should be incremented
*
* @param dateTime Initial {@link ZonedDateTime} instance to use
* @return {@link ZonedDateTime} with the next greater field incremented and all smaller fields cleared
*/
abstract ZonedDateTime overflow(ZonedDateTime dateTime);
}
private final String expr;
private final SimpleField secondField;
private final SimpleField minuteField;
private final SimpleField hourField;
private final DayOfWeekField dayOfWeekField;
private final SimpleField monthField;
private final DayOfMonthField dayOfMonthField;
public CronExpression(final String expr) {
this(expr, true);
}
public CronExpression(final String expr, final boolean withSeconds) {
if (expr == null) {
throw new IllegalArgumentException("expr is null"); //$NON-NLS-1$
}
this.expr = expr;
final int expectedParts = withSeconds ? 6 : 5;
final String[] parts = expr.split("\\s+"); //$NON-NLS-1$
if (parts.length != expectedParts) {
throw new IllegalArgumentException(String.format("Invalid cron expression [%s], expected %s field, got %s", expr, expectedParts, parts.length));
}
int ix = withSeconds ? 1 : 0;
this.secondField = new SimpleField(CronFieldType.SECOND, withSeconds ? parts[0] : "0");
this.minuteField = new SimpleField(CronFieldType.MINUTE, parts[ix++]);
this.hourField = new SimpleField(CronFieldType.HOUR, parts[ix++]);
this.dayOfMonthField = new DayOfMonthField(parts[ix++]);
this.monthField = new SimpleField(CronFieldType.MONTH, parts[ix++]);
this.dayOfWeekField = new DayOfWeekField(parts[ix]);
}
public static CronExpression create(final String expr) {
return new CronExpression(expr, true);
}
public static CronExpression createWithoutSeconds(final String expr) {
return new CronExpression(expr, false);
}
public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime) {
// will search for the next time within the next 4 years. If there is no
// time matching, an InvalidArgumentException will be thrown (it is very
// likely that the cron expression is invalid, like the February 30th).
return nextTimeAfter(afterTime, afterTime.plusYears(4));
}
public LocalDateTime nextLocalDateTimeAfter(LocalDateTime dateTime) {
return nextTimeAfter(ZonedDateTime.of(dateTime, ZoneId.systemDefault())).toLocalDateTime();
}
public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime, long durationInMillis) {
// will search for the next time within the next durationInMillis
// millisecond. Be aware that the duration is specified in millis,
// but in fact the limit is checked on a day-to-day basis.
return nextTimeAfter(afterTime, afterTime.plus(Duration.ofMillis(durationInMillis)));
}
public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime, ZonedDateTime dateTimeBarrier) {
ZonedDateTime[] nextDateTime = { afterTime.plusSeconds(1).withNano(0) };
while (true) {
checkIfDateTimeBarrierIsReached(nextDateTime[0], dateTimeBarrier);
if (!monthField.nextMatch(nextDateTime)) {
continue;
}
if (!findDay(nextDateTime)) {
continue;
}
if (!hourField.nextMatch(nextDateTime)) {
continue;
}
if (!minuteField.nextMatch(nextDateTime)) {
continue;
}
if (!secondField.nextMatch(nextDateTime)) {
continue;
}
checkIfDateTimeBarrierIsReached(nextDateTime[0], dateTimeBarrier);
return nextDateTime[0];
}
}
/**
* Find the next match for the day field.
* <p>
* This is handled different than all other fields because there are two ways to describe the day and it is easier
* to handle them together in the same method.
*
* @param dateTime Initial {@link ZonedDateTime} instance to start from
* @return {@code true} if a match was found for this field or {@code false} if the field overflowed
* @see SimpleField#nextMatch(ZonedDateTime[])
*/
private boolean findDay(ZonedDateTime[] dateTime) {
int month = dateTime[0].getMonthValue();
while (!(dayOfMonthField.matches(dateTime[0].toLocalDate())
&& dayOfWeekField.matches(dateTime[0].toLocalDate()))) {
dateTime[0] = dateTime[0].plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
if (dateTime[0].getMonthValue() != month) {
return false;
}
}
return true;
}
private static void checkIfDateTimeBarrierIsReached(ZonedDateTime nextTime, ZonedDateTime dateTimeBarrier) {
if (nextTime.isAfter(dateTimeBarrier)) {
throw new IllegalArgumentException("No next execution time could be determined that is before the limit of " + dateTimeBarrier);
}
}
@Override
public String toString() {
return getClass().getSimpleName() + "<" + expr + ">";
}
static class FieldPart implements Comparable<FieldPart> {
private int from = -1, to = -1, increment = -1;
private String modifier, incrementModifier;
@Override
public int compareTo(FieldPart o) {
return Integer.compare(from, o.from);
}
}
abstract static class BasicField {
@SuppressWarnings({"RegExpRepeatedSpace", "RegExpSimplifiable", "RegExpSingleCharAlternation", "RegExpRedundantEscape"})
private static final Pattern CRON_FIELD_REGEXP = Pattern
.compile("""
(?: # start of group 1
(?:(?<all>\\*)|(?<ignore>\\?)|(?<last>L)) # global flag (L, ?, *)
| (?<start>[0-9]{1,2}|[a-z]{3,3}) # or start number or symbol
(?: # start of group 2
(?<mod>L|W) # modifier (L,W)
| -(?<end>[0-9]{1,2}|[a-z]{3,3}) # or end nummer or symbol (in range)
)? # end of group 2
) # end of group 1
(?:(?<incmod>/|\\#)(?<inc>[0-9]{1,7}))? # increment and increment modifier (/ or \\#)
""",
Pattern.CASE_INSENSITIVE | Pattern.COMMENTS);
final CronFieldType fieldType;
final List<FieldPart> parts = new ArrayList<>();
private BasicField(CronFieldType fieldType, String fieldExpr) {
this.fieldType = fieldType;
parse(fieldExpr);
}
private void parse(String fieldExpr) { // NOSONAR
String[] rangeParts = fieldExpr.split(",");
for (String rangePart : rangeParts) {
Matcher m = CRON_FIELD_REGEXP.matcher(rangePart);
if (!m.matches()) {
throw new IllegalArgumentException("Invalid cron field '" + rangePart + "' for field [" + fieldType + "]");
}
String startNummer = m.group("start");
String modifier = m.group("mod");
String sluttNummer = m.group("end");
String incrementModifier = m.group("incmod");
String increment = m.group("inc");
FieldPart part = new FieldPart();
part.increment = 999;
if (startNummer != null) {
part.from = mapValue(startNummer);
part.modifier = modifier;
if (sluttNummer != null) {
part.to = mapValue(sluttNummer);
part.increment = 1;
} else if (increment != null) {
part.to = fieldType.to;
} else {
part.to = part.from;
}
} else if (m.group("all") != null) {
part.from = fieldType.from;
part.to = fieldType.to;
part.increment = 1;
} else if (m.group("ignore") != null) {
part.modifier = m.group("ignore");
} else if (m.group("last") != null) {
part.modifier = m.group("last");
} else {
throw new IllegalArgumentException("Invalid cron part: " + rangePart);
}
if (increment != null) {
part.incrementModifier = incrementModifier;
part.increment = Integer.parseInt(increment);
}
validateRange(part);
validatePart(part);
parts.add(part);
}
Collections.sort(parts);
}
protected void validatePart(FieldPart part) {
if (part.modifier != null) {
throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier));
} else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) {
throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier));
}
}
private void validateRange(FieldPart part) {
if ((part.from != -1 && part.from < fieldType.from) || (part.to != -1 && part.to > fieldType.to)) {
throw new IllegalArgumentException(String.format("Invalid interval [%s-%s], must be %s<=_<=%s", part.from, part.to, fieldType.from,
fieldType.to));
} else if (part.from != -1 && part.to != -1 && part.from > part.to) {
throw new IllegalArgumentException(
String.format(
"Invalid interval [%s-%s]. Rolling periods are not supported (ex. 5-1, only 1-5) since this won't give a deterministic result. Must be %s<=_<=%s",
part.from, part.to, fieldType.from, fieldType.to));
}
}
protected int mapValue(String value) {
int idx;
if (fieldType.names != null && (idx = fieldType.names.indexOf(value.toUpperCase(Locale.getDefault()))) >= 0) {
return idx + fieldType.from;
}
return Integer.parseInt(value);
}
protected boolean matches(int val, FieldPart part) {
return val >= part.from && val <= part.to && (val - part.from) % part.increment == 0;
}
protected int nextMatch(int val, FieldPart part) {
if (val > part.to) {
return -1;
}
int nextPotential = Math.max(val, part.from);
if (part.increment == 1 || nextPotential == part.from) {
return nextPotential;
}
int remainder = ((nextPotential - part.from) % part.increment);
if (remainder != 0) {
nextPotential += part.increment - remainder;
}
return nextPotential <= part.to ? nextPotential : -1;
}
}
static class SimpleField extends BasicField {
SimpleField(CronFieldType fieldType, String fieldExpr) {
super(fieldType, fieldExpr);
}
/**
* Find the next match for this field. If a match cannot be found force an overflow and increase the next
* greatest field.
*
* @param dateTime {@link ZonedDateTime} array so the reference can be modified
* @return {@code true} if a match was found for this field or {@code false} if the field overflowed
*/
protected boolean nextMatch(ZonedDateTime[] dateTime) {
int value = fieldType.getValue(dateTime[0]);
for (FieldPart part : parts) {
int nextMatch = nextMatch(value, part);
if (nextMatch > -1) {
if (nextMatch != value) {
dateTime[0] = fieldType.setValue(dateTime[0], nextMatch);
}
return true;
}
}
dateTime[0] = fieldType.overflow(dateTime[0]);
return false;
}
}
static class DayOfWeekField extends BasicField {
DayOfWeekField(String fieldExpr) {
super(CronFieldType.DAY_OF_WEEK, fieldExpr);
}
boolean matches(LocalDate dato) {
for (FieldPart part : parts) {
if ("L".equals(part.modifier)) {
YearMonth ym = YearMonth.of(dato.getYear(), dato.getMonth().getValue());
return dato.getDayOfWeek() == DayOfWeek.of(part.from) && dato.getDayOfMonth() > (ym.lengthOfMonth() - 7);
} else if ("#".equals(part.incrementModifier)) {
if (dato.getDayOfWeek() == DayOfWeek.of(part.from)) {
int num = dato.getDayOfMonth() / 7;
return part.increment == (dato.getDayOfMonth() % 7 == 0 ? num : num + 1);
}
return false;
} else if (matches(dato.getDayOfWeek().getValue(), part)) {
return true;
}
}
return false;
}
@Override
protected int mapValue(String value) {
// Use 1-7 for weedays, but 0 will also represent sunday (linux practice)
return "0".equals(value) ? 7 : super.mapValue(value);
}
@Override
protected boolean matches(int val, FieldPart part) {
return "?".equals(part.modifier) || super.matches(val, part);
}
@Override
protected void validatePart(FieldPart part) {
if (part.modifier != null && !Arrays.asList("L", "?").contains(part.modifier)) {
throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier));
} else if (part.incrementModifier != null && !Arrays.asList("/", "#").contains(part.incrementModifier)) {
throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier));
}
}
}
static class DayOfMonthField extends BasicField {
DayOfMonthField(String fieldExpr) {
super(CronFieldType.DAY_OF_MONTH, fieldExpr);
}
boolean matches(LocalDate dato) {
for (FieldPart part : parts) {
if ("L".equals(part.modifier)) {
YearMonth ym = YearMonth.of(dato.getYear(), dato.getMonth().getValue());
return dato.getDayOfMonth() == (ym.lengthOfMonth() - (part.from == -1 ? 0 : part.from));
} else if ("W".equals(part.modifier)) {
if (dato.getDayOfWeek().getValue() <= 5) {
if (dato.getDayOfMonth() == part.from) {
return true;
} else if (dato.getDayOfWeek().getValue() == 5) {
return dato.plusDays(1).getDayOfMonth() == part.from;
} else if (dato.getDayOfWeek().getValue() == 1) {
return dato.minusDays(1).getDayOfMonth() == part.from;
}
}
} else if (matches(dato.getDayOfMonth(), part)) {
return true;
}
}
return false;
}
@Override
protected void validatePart(FieldPart part) {
if (part.modifier != null && !Arrays.asList("L", "W", "?").contains(part.modifier)) {
throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier));
} else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) {
throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier));
}
}
@Override
protected boolean matches(int val, FieldPart part) {
return "?".equals(part.modifier) || super.matches(val, part);
}
}
}

View File

@ -3,8 +3,19 @@ package fr.pandacube.lib.util;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.Arrays; import java.util.Arrays;
/**
* This class contains various methods to manipulate and display distances.
*/
public class DistanceUtil { public class DistanceUtil {
/**
* Generate a string representation of the provided distance, with a specific decimal precision and one of the
* specified metric prefix
* @param meterDist the distance to display, in meter
* @param precision the number of digit to display after the decimal separator
* @param desiredUnits the prefered unit prefix to use for convertion.
* @return a string representation of the provided distance
*/
public static String distanceToString(double meterDist, int precision, DistanceUnit... desiredUnits) { public static String distanceToString(double meterDist, int precision, DistanceUnit... desiredUnits) {
Arrays.sort(desiredUnits); Arrays.sort(desiredUnits);
@ -28,20 +39,64 @@ public class DistanceUtil {
return df.format(dist) + choosenUnit.unitStr; return df.format(dist) + choosenUnit.unitStr;
} }
/**
*
* Generate a string representation of the provided distance, with a specific decimal precision, and using either
* {@code km} or {@code m} as a unit.
* <p>
* Calling this method is equivalent to {@code distanceToString(meterDist, precision, DistanceUnit.M, DistanceUnit.KM)}.
* @param meterDist the distance to display, in meter
* @param precision the number of digit to display after the decimal separator
* @return a string representation of the provided distance
*/
public static String distanceToString(double meterDist, int precision) { public static String distanceToString(double meterDist, int precision) {
return distanceToString(meterDist, precision, DistanceUnit.M, DistanceUnit.KM); return distanceToString(meterDist, precision, DistanceUnit.M, DistanceUnit.KM);
} }
/**
* Enumeration of comonly used distance metric unit
*/
public enum DistanceUnit { public enum DistanceUnit {
/**
* Nanometer unit. One billionth of a meter. 10<sup>-9</sup> = 0.000000001m.
*/
NM(0.000000001, "nm"), NM(0.000000001, "nm"),
/**
* Micrometer unit. One millionth of a meter. 10<sup>-6</sup> = 0.000001m.
*/
UM(0.000001, "µm"), UM(0.000001, "µm"),
/**
* Millimeter unit. One thousandth of a meter. 10<sup>-3</sup> = 0.001m.
*/
MM(0.001, "mm"), MM(0.001, "mm"),
/**
* Centimeter unit. One hundredth of a meter. 10<sup>-2</sup> = 0.01m
*/
CM(0.01, "cm"), CM(0.01, "cm"),
/**
* Meter unit. One meter. 10<sup>0</sup> = 1m.
*/
M(1, "m"), M(1, "m"),
/**
* Kilometer unit. One thousand meter. 10<sup>3</sup> = 1000m.
*/
KM(1000, "km"); KM(1000, "km");
private final double multiplicator; /**
private final String unitStr; * The value of this unit in meter.
*/
public final double multiplicator;
/**
* String representation of the unit symbol.
*/
public final String unitStr;
DistanceUnit(double mult, String s) { DistanceUnit(double mult, String s) {
multiplicator = mult; multiplicator = mult;

View File

@ -3,6 +3,9 @@ package fr.pandacube.lib.util;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* This class provides methods for manipulating enums.
*/
public class EnumUtil { public class EnumUtil {
/** /**
@ -11,6 +14,7 @@ public class EnumUtil {
* @param enumType the enum class. * @param enumType the enum class.
* @param separator a string which will be used as a separator * @param separator a string which will be used as a separator
* @return a string representation of the enum class. * @return a string representation of the enum class.
* @param <T> the type of the enum.
*/ */
public static <T extends Enum<T>> String enumList(Class<T> enumType, String separator) { public static <T extends Enum<T>> String enumList(Class<T> enumType, String separator) {
return Arrays.stream(enumType.getEnumConstants()) return Arrays.stream(enumType.getEnumConstants())
@ -19,67 +23,65 @@ public class EnumUtil {
} }
/** /**
* List all enum constants which are in the specified enum class. It is * List all enum constants which are in the specified enum class.
* equivalent to call * It is equivalent to call {@link #enumList(Class, String)} with the second parameter <code>", "</code>.
* {@link #enumList(Class, String)} with the second parameter
* <code>", "</code>
* *
* @param enumType the enum class. * @param enumType the enum class.
* @return a string representation of the enum class. * @return a string representation of the enum class.
* @param <T> the type of the enum.
*/ */
public static <T extends Enum<T>> String enumList(Class<T> enumType) { public static <T extends Enum<T>> String enumList(Class<T> enumType) {
return enumList(enumType, ", "); return enumList(enumType, ", ");
} }
/** /**
* Permet de rechercher l'existance d'un élément dans un enum, de façon * Search for a specific enum entry in the provided enum type, using the case-insensitive search string.
* insensible à la casse
* *
* @param enumType la classe correpondant à l'enum à lister * @param enumType the class of the enum in which to search
* @param search l'élément à rechercher, insensible à la casse * @param search the case-insensitive name of the enum value to return.
* @return l'élément de l'énumarétion, si elle a été trouvée, null sinon * @return the element found in the enum, or null if not found.
* @param <T> the type of the enum.
*/ */
public static <T extends Enum<T>> T searchEnum(Class<T> enumType, String search) { public static <T extends Enum<T>> T searchEnum(Class<T> enumType, String search) {
T[] elements = enumType.getEnumConstants(); for (T el : enumType.getEnumConstants()) {
if (el.name().equalsIgnoreCase(search)) {
for (T el : elements) return el;
if (el.name().equalsIgnoreCase(search)) return el; }
}
return null; return null;
} }
/** /**
* Permet de rechercher l'existance d'un élément dans un enum, de façon * Search for a specific enum entry in the provided enum type, using the case-insensitive search string.
* insensible à la casse * unlike {@link #searchEnum(Class, String)}, this method does not statically check the enum type, in case it is not
* La validité de la classe passé en premier paramètre est vérifiée * known at compilation time.
* dynamiquement et non
* statiquement. Préférez l'utilisation de
* {@link #searchEnum(Class, String)} quand c'est possible.
* *
* @param enumType la classe correpondant à l'enum à lister * For a statically checked enum type, uses {@link #searchEnum(Class, String)} instead.
* @param search l'élément à rechercher, insensible à la casse *
* @return l'élément de l'énumération, si elle a été trouvée et si la classe * @param enumType the class of the enum in which to search
* passée en paramètre est un enum, null dans les autres cas * @param search the case-insensitive name of the enum value to return.
* @return the element found in the enum, or null if not found or if the provideid type is not an enum.
*/ */
public static Enum<?> searchUncheckedEnum(Class<?> enumType, String search) { public static Enum<?> searchUncheckedEnum(Class<?> enumType, String search) {
if (!enumType.isEnum()) return null; if (!enumType.isEnum())
Enum<?>[] elements = (Enum<?>[]) enumType.getEnumConstants(); return null;
for (Enum<?> el : (Enum<?>[]) enumType.getEnumConstants()) {
for (Enum<?> el : elements) if (el.name().equalsIgnoreCase(search)) {
if (el.name().equalsIgnoreCase(search)) return el; return el;
}
}
return null; return null;
} }
/** /**
* Retourne une valeur aléatoire parmis les élément de l'Enum spécifié en * Pick a random value from an enum type.
* paramètre.
* *
* @param enumType l'enum dans lequel piocher la valeur * @param enumType the class of the enum in which to pick the value from
* @return une des valeurs de l'enum * @return one of the enum value, or null if the provided enum is empty.
* @param <T> the type of the enum.
*/ */
public static <T extends Enum<T>> T randomValue(Class<T> enumType) { public static <T extends Enum<T>> T randomValue(Class<T> enumType) {
T[] elements = enumType.getEnumConstants(); return RandomUtil.arrayElement(enumType.getEnumConstants());
return elements[RandomUtil.rand.nextInt(elements.length)];
} }
} }

View File

@ -5,10 +5,16 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
/**
* Provides utility methods to manipulate files and directories
*/
public class FileUtils { public class FileUtils {
/**
* Recursively delete the provided file and all of its content if it is a directory.
* @param target the target file or directory.
*/
public static void delete(File target) { public static void delete(File target) {
if (target.isDirectory()) if (target.isDirectory())
for (File child : target.listFiles()) for (File child : target.listFiles())
@ -16,8 +22,21 @@ public class FileUtils {
target.delete(); target.delete();
} }
/**
* Recursively copy the provided source file or directory to the provided target.
* @param source the source file or directory.
* @param target the copy destination.
* @throws IOException if an IO error occurs.
* @throws IllegalStateException if the target destination already exists and is not a directory.
* @throws IllegalArgumentException if at least one of the parameter is null, or if the source doesnt exists.
*/
public static void copy(File source, File target) throws IOException { public static void copy(File source, File target) throws IOException {
if (source == null || !source.exists()) {
throw new IllegalArgumentException("source is null or doesnt exists: " + source);
}
if (target == null) {
throw new IllegalArgumentException("target cannot be null");
}
if (target.exists() && !target.isDirectory()) { if (target.exists() && !target.isDirectory()) {
throw new IllegalStateException("target file already exists: " + target); throw new IllegalStateException("target file already exists: " + target);
} }

View File

@ -6,69 +6,103 @@ import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
/**
* An {@link Iterator} that iterate over all the elements of all the provided iterators.
* In other words, this class concatenate the provided iterators.
* @param <T> the type of the values in the iterators
*/
public class IteratorIterator<T> implements Iterator<T> { public class IteratorIterator<T> implements Iterator<T> {
/**
* Create an {@link IteratorIterator} with the provided {@link Collection} of {@link Iterable}.
* The iterables iterators will be concatenated in the order of the collections iterator.
* @param coll the collection of iterables.
* @return a new instance of {@link IteratorIterator} iterating over the elements of the provided iterables.
* @param <T> the type of the values in the iterables.
*/
public static <T> IteratorIterator<T> ofCollectionOfIterable(Collection<Iterable<T>> coll) { public static <T> IteratorIterator<T> ofCollectionOfIterable(Collection<Iterable<T>> coll) {
return new IteratorIterator<>(coll.stream().map(Iterable::iterator).iterator()); return new IteratorIterator<>(coll.stream().map(Iterable::iterator).iterator());
} }
/**
* Create an {@link IteratorIterator} with the provided {@link Collection} of {@link Iterator}.
* The iterators will be concatenated in the order of the collections iterator.
* @param coll the collection of iterators.
* @return a new instance of {@link IteratorIterator} iterating over the elements of the provided iterators.
* @param <T> the type of the values in the iterators.
*/
public static <T> IteratorIterator<T> ofCollectionOfIterator(Collection<Iterator<T>> coll) { public static <T> IteratorIterator<T> ofCollectionOfIterator(Collection<Iterator<T>> coll) {
return new IteratorIterator<>(new ArrayList<>(coll).iterator()); return new IteratorIterator<>(new ArrayList<>(coll).iterator());
} }
/**
* Create an {@link IteratorIterator} with the provided array of {@link Iterable}.
* The iterables iterators will be concatenated in the order of the array.
* @param arr the array of iterables.
* @return a new instance of {@link IteratorIterator} iterating over the elements of the provided iterables.
* @param <T> the type of the values in the iterables.
*/
@SafeVarargs @SafeVarargs
public static <T> IteratorIterator<T> ofArrayOfIterable(Iterable<T>... arr) { public static <T> IteratorIterator<T> ofArrayOfIterable(Iterable<T>... arr) {
return new IteratorIterator<>(Arrays.stream(arr).map(Iterable::iterator).iterator()); return new IteratorIterator<>(Arrays.stream(arr).map(Iterable::iterator).iterator());
} }
/**
* Create an {@link IteratorIterator} with the provided array of {@link Iterator}.
* The iterators will be concatenated in the order of the array.
* @param arr the array of iterators.
* @return a new instance of {@link IteratorIterator} iterating over the elements of the provided iterators.
* @param <T> the type of the values in the iterators.
*/
@SafeVarargs @SafeVarargs
public static <T> IteratorIterator<T> ofArrayOfIterator(Iterator<T>... arr) { public static <T> IteratorIterator<T> ofArrayOfIterator(Iterator<T>... arr) {
return new IteratorIterator<>(Arrays.asList(arr).iterator()); return new IteratorIterator<>(Arrays.asList(arr).iterator());
} }
private final Iterator<Iterator<T>> iterators; private final Iterator<Iterator<T>> iterators;
private Iterator<T> currentIterator = null; private Iterator<T> currentValueIterator = null;
private Iterator<T> nextValueIterator = null;
private IteratorIterator(Iterator<Iterator<T>> iterators) { private IteratorIterator(Iterator<Iterator<T>> iterators) {
this.iterators = iterators; this.iterators = iterators;
} }
private void fixCurrentIterator() { private void fixNextIterator() {
if (currentIterator != null && !currentIterator.hasNext()) { if (nextValueIterator != null && !nextValueIterator.hasNext()) {
currentIterator = null; nextValueIterator = null;
} }
} }
private void fixState() { private void fixState() {
fixCurrentIterator(); fixNextIterator();
while (currentIterator == null && iterators.hasNext()) { while (nextValueIterator == null && iterators.hasNext()) {
currentIterator = iterators.next(); nextValueIterator = iterators.next();
fixCurrentIterator(); fixNextIterator();
} }
} }
@Override @Override
public boolean hasNext() { public boolean hasNext() {
fixState(); fixState();
return currentIterator != null && currentIterator.hasNext(); return nextValueIterator != null && nextValueIterator.hasNext();
} }
@Override @Override
public T next() { public T next() {
if (!hasNext()) if (!hasNext())
throw new NoSuchElementException("No next value found in iterator."); throw new NoSuchElementException("No next value found in iterator.");
return currentIterator.next(); currentValueIterator = nextValueIterator;
return currentValueIterator.next();
} }
/**
* @implNote The current implementation of {@link IteratorIterator} may not support
* running this method if the current position is the last value of one of
* the underlying iterable, and if the {@link #hasNext()} method has been called before this one.
*/
@Override @Override
public void remove() { public void remove() {
// TODO change code to avoid currentIterator being null because we are about to change currentIterator if (currentValueIterator == null)
if (currentIterator != null) throw new IllegalStateException();
currentIterator.remove(); currentValueIterator.remove();
} }
} }

View File

@ -1,736 +0,0 @@
package fr.pandacube.lib.util;
//******************************************************************************
//***
//*** INTERPRETEUR ARITHMETIQUE version 2.1 Version Java
//***
//***
//***
//******************************************************************************
/*
Historique:
2.1:
- Version Java disponible:
# les operateurs mathematiques disponibles sont ceux de Java donc certains manquent.
2.0:
- Portage en C++ et debut en Java
Version C++:
- Meilleure gestion memoire lors de la construction de l'expression.
- Acceleration de certaines operations.
Version Java:
- Premiere version. Normalement ca doit marcher
1.3b: ajoute les fonctions suivantes: (NON DISTRIBUEE)
- reconnaissance du symbole <EFBFBD>
- ecriture formatee d'expression (<EFBFBD> ameliorer)
1.2: corrige les bugs suivants:
- erreur sur l'interpretation de fonctions unaires imbriquees telles que ln(exp(x)).
- la fonction puissance (pour les puissances entieres).
ajoute:
- la verification de la chaine entree (voir code d'erreur)
- la verification de l'existence de l'evaluation (voir code d'erreur)
1.1: corrige un bug au niveau de l'interpretation des fonctions du type:
exp(-(x+y))
1.0: premiere version
Le code source peut etre librement modifie et distribue.
Puisqu'il s'agit d'un essai en Java, le code ne doit pas etre super genial et merite sans doute
quelques modifications.En particulier, il serait interessant de rajouter le support des Exceptions
pour les calculs (division par zero, etc...).
*/
// Classe servant à palier l'absence de passage par variables ou reference
class VariableInt {
public int mValue;
}
// Classe principale
public class JArithmeticInterpreter {
// Variables
final int mOperator;
final double mValue;
JArithmeticInterpreter fg, fd;
// Methods
// ....................................................................................
// Node
private JArithmeticInterpreter() {
this(0, 0);
}
// ....................................................................................
// Node
private JArithmeticInterpreter(int Operator, double Value) {
mOperator = Operator;
mValue = Value;
}
// ....................................................................................
// Construct_Tree
private static JArithmeticInterpreter constructTree(StringBuffer string, int length) {
int imbric, Bimbric;
int priorite, ope;
int position, positionv, i, j;
int opetemp = 0;
int espa = 0, espat = 0;
int caspp = 0;
JArithmeticInterpreter node;
// Initialisation des variables
if (length <= 0) {
return null;
}
ope = 0;
imbric = 0;
Bimbric = 128;
priorite = 6;
i = 0;
positionv = position = 0;
// Mise en place des donnees sur le morceau de chaine
while (i < length)
if (((string.charAt(i) > 47) && (string.charAt(i) < 58)) || (string.charAt(i) == '<27>')) {
if (priorite > 5) {
priorite = 5;
positionv = i;
}
i++;
}
else if ((string.charAt(i) > 96) && (string.charAt(i) < 117)) {
VariableInt Vopetemp, Vespat;
Vopetemp = new VariableInt();
Vespat = new VariableInt();
Vopetemp.mValue = opetemp;
Vespat.mValue = espat;
FindOperator(Vopetemp, Vespat, string, i);
opetemp = Vopetemp.mValue;
espat = Vespat.mValue;
if (opetemp >= 0) {
if (imbric < Bimbric) {
Bimbric = imbric;
ope = opetemp;
position = i;
priorite = 4;
espa = espat;
}
else if ((imbric == Bimbric) && (priorite >= 4)) {
ope = opetemp;
position = i;
priorite = 4;
espa = espat;
}
j = i + 1;
i += espat;
while (j < i)
j++;
}
else if (string.charAt(i) == 't') {
if (priorite == 6) ope = -1;
i++;
}
else if (string.charAt(i) == 'p') {
if (priorite == 6) ope = -2;
i++;
}
else if (string.charAt(i) == 'r') {
if (priorite == 6) ope = -2;
i++;
}
else if (string.charAt(i) == 'n') {
if (priorite == 6) ope = -1;
i++;
}
else {
return null;
}
}
else
switch (string.charAt(i)) {
case '(':
imbric++;
i++;
break;
case ')':
imbric--;
i++;
break;
case '+':
if (imbric < Bimbric) {
Bimbric = imbric;
priorite = 1;
ope = 1;
position = i;
caspp = 0;
}
else if (imbric == Bimbric) {
priorite = 1;
ope = 1;
position = i;
caspp = 0;
}
i++;
break;
case '-':
if (imbric < Bimbric) {
if ((i - 1) < 0) {
if (priorite > 5) {
priorite = 1;
position = i;
ope = 2;
Bimbric = imbric;
caspp = 1;
}
}
else if (string.charAt(i - 1) == '(') {
if (priorite > 1) {
priorite = 1;
position = i;
Bimbric = imbric;
caspp = 1;
ope = 2;
}
}
else {
Bimbric = imbric;
priorite = 1;
ope = 2;
position = i;
caspp = 0;
}
}
else if (imbric == Bimbric) if ((i - 1) < 0) {
if (priorite > 5) {
priorite = 1;
position = i;
ope = 2;
caspp = 1;
}
}
else if (string.charAt(i - 1) == '(') {
if (priorite > 1) {
priorite = 1;
position = i;
caspp = 1;
ope = 2;
}
}
else {
priorite = 1;
ope = 2;
position = i;
caspp = 0;
}
i++;
break;
case '*':
if (imbric < Bimbric) {
Bimbric = imbric;
priorite = 2;
ope = 3;
position = i;
}
else if ((imbric == Bimbric) && (priorite >= 2)) {
priorite = 2;
ope = 3;
position = i;
}
i++;
break;
case '/':
if (imbric < Bimbric) {
Bimbric = imbric;
priorite = 2;
ope = 4;
position = i;
}
else if ((imbric == Bimbric) && (priorite >= 2)) {
priorite = 2;
ope = 4;
position = i;
}
i++;
break;
case '^':
if (imbric < Bimbric) {
Bimbric = imbric;
priorite = 3;
ope = 5;
position = i;
}
else if ((imbric == Bimbric) && (priorite >= 3)) {
priorite = 3;
ope = 5;
position = i;
}
i++;
break;
case '.':
i++;
break;
default:
return null;
}
if (imbric != 0) {
return null;
}
// Traitement des donnees
if (priorite == 6) {
node = new JArithmeticInterpreter(ope, 0.0);
return node;
}
else if (caspp == 1) {
node = new JArithmeticInterpreter(2, 0);
node.fg = new JArithmeticInterpreter(0, 0);
node.fd = new JArithmeticInterpreter();
if ((length - position - 1 - Bimbric) == 0) { // argument absent
return null;
}
StringBuffer temp = CopyPartialString(string, (position + 1), (length - 1 - Bimbric));
node.fd = constructTree(temp, (length - position - 1 - Bimbric));
return node;
}
else if (priorite == 5) {
node = new JArithmeticInterpreter(0, calc_const(string, positionv));
return node;
}
else if (ope > 5) {
node = new JArithmeticInterpreter(ope, 0);
if ((length - position - espa - Bimbric) == 0) { // argument absent
return null;
}
StringBuffer temp = CopyPartialString(string, (position + espa), (length - 1));
node.fg = constructTree(temp, (length - position - espa - Bimbric));
return node;
}
else {
node = new JArithmeticInterpreter(ope, 0);
if ((position - Bimbric) == 0) { // argument absent
return null;
}
StringBuffer temp = CopyPartialString(string, Bimbric, (position - 1));
node.fg = constructTree(temp, (position - Bimbric));
if ((length - position - 1 - Bimbric) == 0) { // argument absent
return null;
}
temp = CopyPartialString(string, (position + 1), (length - 1 - Bimbric));
node.fd = constructTree(temp, (length - position - 1 - Bimbric));
return node;
}
}
// ....................................................................................
private double computeTree() {
if (mOperator == 0) return mValue;
double valueL = fg.computeTree();
double valueR = 0;
if (fd != null) valueR = fd.computeTree();
switch (mOperator) {
case 1: // +
return (valueL + valueR);
case 2: // -
return (valueL - valueR);
case 3: // *
return (valueL * valueR);
case 4: // -
if (valueR == 0) {
return 0;
}
return (valueL / valueR);
case 5: // ^
return Math.pow(valueL, valueR);
case 6: // exp
return Math.exp(valueL);
case 7: // ln
if (valueL <= 0) {
return 0;
}
return (Math.log(valueL) / Math.log(2));
case 8: // log
if (valueL <= 0) {
return 0;
}
return Math.log(valueL);
case 9: // sqrt
if (valueL < 0) {
return 0;
}
return Math.sqrt(valueL);
case 10: // abs
return Math.abs(valueL);
case 11:
return Math.sin(valueL); // sin
case 12:
return Math.cos(valueL); // cos
case 13:
return Math.tan(valueL); // tan
case 14:
return Math.asin(valueL); // asin
case 15:
return Math.acos(valueL); // acos
case 16:
return Math.atan(valueL); // atan
default:
return 0;
}
}
// ....................................................................................
// Write_Tree
private void writeTree(StringBuffer string) {
boolean parenthese = false;
switch (mOperator) {
case 0:
string.append(StringUtil.formatDouble(mValue));
break;
case 1:
fg.writeTree(string);
string.append('+');
fd.writeTree(string);
break;
case 2:
if (fg.mOperator != 0 || fg.mValue != 0)
fg.writeTree(string);
string.append('-');
if ((fd.mOperator == 1) || (fd.mOperator == 2)) {
parenthese = true;
string.append('(');
}
fd.writeTree(string);
if (parenthese) string.append(')');
break;
case 3:
if ((fg.mOperator == 1) || (fg.mOperator == 2)) {
parenthese = true;
string.append('(');
}
fg.writeTree(string);
if (parenthese) string.append(')');
parenthese = false;
string.append('*');
if ((fd.mOperator == 1) || (fd.mOperator == 2)) {
parenthese = true;
string.append('(');
}
fd.writeTree(string);
if (parenthese) string.append(')');
break;
case 4:
if ((fg.mOperator == 1) || (fg.mOperator == 2)) {
parenthese = true;
string.append('(');
}
fg.writeTree(string);
if (parenthese) string.append(')');
parenthese = false;
string.append('/');
if ((fd.mOperator > 0) && (fd.mOperator < 5)) {
parenthese = true;
string.append('(');
}
fd.writeTree(string);
if (parenthese) string.append(')');
break;
case 5:
if ((fg.mOperator > 0) && (fg.mOperator < 5)) {
parenthese = true;
string.append('(');
}
fg.writeTree(string);
if (parenthese) string.append(')');
parenthese = false;
string.append('^');
if ((fd.mOperator > 0) && (fd.mOperator < 5)) {
parenthese = true;
string.append('(');
}
fd.writeTree(string);
if (parenthese) string.append(')');
break;
case 6:
string.append("exp(");
fg.writeTree(string);
string.append(')');
break;
case 7:
string.append("log(");
fg.writeTree(string);
string.append(')');
break;
case 8:
string.append("ln(");
fg.writeTree(string);
string.append(')');
break;
case 9:
string.append("sqrt(");
fg.writeTree(string);
string.append(')');
break;
case 10:
string.append("|");
fg.writeTree(string);
string.append('|');
break;
case 11:
string.append("sin(");
fg.writeTree(string);
string.append(')');
break;
case 12:
string.append("cos(");
fg.writeTree(string);
string.append(')');
break;
case 13:
string.append("tan(");
fg.writeTree(string);
string.append(')');
break;
case 14:
string.append("asin(");
fg.writeTree(string);
string.append(')');
break;
case 15:
string.append("acos(");
fg.writeTree(string);
string.append(')');
break;
case 16:
string.append("atan(");
fg.writeTree(string);
string.append(')');
break;
}
}
// ....................................................................................
// calc_const
private static double calc_const(StringBuffer chaine, int pos) {
int i = pos, j;
double temp = 0;
int signe = 1;
int longueur = chaine.length();
if (chaine.charAt(i) == '-') {
signe = -1;
i++;
}
if (chaine.charAt(i) == 'π') return signe * Math.PI;
while (i < longueur && chaine.charAt(i) > 47 && chaine.charAt(i) < 58) {
temp = temp * 10 + (chaine.charAt(i) - 48);
i++;
}
if (i < longueur && chaine.charAt(i) == '.') {
i++;
j = 1;
while (i < longueur && chaine.charAt(i) > 47 && chaine.charAt(i) < 58) {
temp = temp + (chaine.charAt(i) - 48) * Math.exp(-j * 2.30258509);
i++;
j++;
}
}
return (signe * temp);
}
// ....................................................................................
// FindOperator
private static void FindOperator(VariableInt oper, VariableInt esp, StringBuffer chaine, int pos) {
switch (chaine.charAt(pos)) {
case 'a':
switch (chaine.charAt(pos + 1)) {
case 'b':
esp.mValue = 3;
oper.mValue = 10;
break;
case 'c':
esp.mValue = 4;
oper.mValue = 15;
break;
case 's':
esp.mValue = 4;
oper.mValue = 14;
break;
case 't':
esp.mValue = 4;
oper.mValue = 16;
break;
}
break;
case 'c':
if (chaine.charAt(pos + 1) == 'h') {
esp.mValue = 2;
oper.mValue = 18;
}
else if ((chaine.charAt(pos + 1) == 'o') && (chaine.charAt(pos + 2) == 's'))
if (chaine.charAt(pos + 3) == 'h') {
esp.mValue = 4;
oper.mValue = 18;
}
else {
esp.mValue = 3;
oper.mValue = 12;
}
break;
case 'e':
if ((chaine.charAt(pos + 1) == 'x') && (chaine.charAt(pos + 2) == 'p')) {
esp.mValue = 3;
oper.mValue = 6;
}
else
oper.mValue = -10;
break;
case 'l':
if (chaine.charAt(pos + 1) == 'n') {
esp.mValue = 2;
oper.mValue = 7;
}
else if ((chaine.charAt(pos + 1) == 'o') && (chaine.charAt(pos + 2) == 'g')) {
esp.mValue = 3;
oper.mValue = 8;
}
else
oper.mValue = -10;
break;
case 's':
if (chaine.charAt(pos + 1) == 'h') {
esp.mValue = 2;
oper.mValue = 17;
}
else if (chaine.charAt(pos + 1) == 'q') {
esp.mValue = 4;
oper.mValue = 9;
}
else if (chaine.charAt(pos + 3) == 'h') {
esp.mValue = 4;
oper.mValue = 17;
}
else {
esp.mValue = 3;
oper.mValue = 11;
}
break;
case 't':
if (chaine.charAt(pos + 1) == 'h') {
esp.mValue = 2;
oper.mValue = 19;
}
else if ((chaine.charAt(pos + 1) == 'a') && (chaine.charAt(pos + 2) == 'n')) {
if (chaine.charAt(pos + 3) == 'h') {
esp.mValue = 4;
oper.mValue = 19;
}
else {
esp.mValue = 3;
oper.mValue = 13;
}
}
else
oper.mValue = -10;
break;
default:
oper.mValue = -10;
break;
}
}
// ....................................................................................
// CopyPartialString
private static StringBuffer CopyPartialString(StringBuffer chaine, int debut, int fin) {
StringBuffer chartemp;
int a = fin - debut + 1;
chartemp = new StringBuffer(a + 1);
for (int i = 0; i < a; i++)
chartemp.append(chaine.charAt(debut + i));
return chartemp;
}
public static double getResultFromExpression(String expr, StringBuffer writeTree) {
StringBuffer input = new StringBuffer(expr);
JArithmeticInterpreter jai = null;
try {
jai = JArithmeticInterpreter.constructTree(input, input.length());
} catch (Exception ignored) {}
if (jai == null) throw new IllegalArgumentException("Le calcul passé en paramètre est invalide");
if (writeTree != null) {
writeTree.setLength(0);
jai.writeTree(writeTree);
}
return jai.computeTree();
}
public static double getResultFromExpression(String expr) {
return getResultFromExpression(expr, null);
}
public static void main(String[] args) {
StringBuffer b = new StringBuffer(0);
String disp_res = StringUtil.formatDouble(JArithmeticInterpreter.getResultFromExpression("1245.25*2", b));
System.out.println(disp_res);
System.out.println(b);
} // */
}

View File

@ -18,6 +18,10 @@ public class Lazy<T> implements Supplier<T> {
private final Supplier<T> supplier; private final Supplier<T> supplier;
private boolean cached = false; private boolean cached = false;
/**
* Create a lazy value loader that will call the provided supplier to get the value.
* @param s the supplier from which the value is fetched.
*/
public Lazy(Supplier<T> s) { public Lazy(Supplier<T> s) {
supplier = Objects.requireNonNull(s); supplier = Objects.requireNonNull(s);
} }
@ -25,21 +29,28 @@ public class Lazy<T> implements Supplier<T> {
/** /**
* Get the wrapped value, from cache or from the provider if it is not yet cached. * Get the wrapped value, from cache or from the provider if it is not yet cached.
* *
* If the provider throws an exception, it will be redirected to the caller as is, and no value will be cached (the next call to this method will * If the provider throws an exception, it will be redirected to the caller as is, and no value will be cached
* execute the supplier again). * (the next call to this method will execute the supplier again).
*/ */
@Override @Override
public synchronized T get() { public synchronized T get() {
if (!cached) // check outside synchronized method to reduce useless synchronization if value is already cached if (!cached)
set(supplier.get()); set(supplier.get());
return cachedValue; return cachedValue;
} }
/**
* Reset the cached value. The next call to {@link #get()} will get the value from the provider.
*/
public synchronized void reset() { public synchronized void reset() {
cached = false; cached = false;
cachedValue = null; cachedValue = null;
} }
/**
* Manually set the value to the provided one.
* @param value the value to put in the cache.
*/
public synchronized void set(T value) { public synchronized void set(T value) {
cachedValue = value; cachedValue = value;
cached = true; cached = true;

View File

@ -13,12 +13,16 @@ import fr.pandacube.lib.util.ThrowableUtil.SupplierException;
* *
* @param <T> the type of the enclosed value. * @param <T> the type of the enclosed value.
*/ */
public class LazyOrException<T> { public class LazyOrException<T> implements SupplierException<T> {
private T cachedValue; private T cachedValue;
private final SupplierException<T> supplier; private final SupplierException<T> supplier;
private boolean cached = false; private boolean cached = false;
/**
* Create a lazy value loader that will call the provided supplier to get the value.
* @param s the supplier from which the value is fetched.
*/
public LazyOrException(SupplierException<T> s) { public LazyOrException(SupplierException<T> s) {
supplier = Objects.requireNonNull(s); supplier = Objects.requireNonNull(s);
} }
@ -26,20 +30,28 @@ public class LazyOrException<T> {
/** /**
* Get the wrapped value, from cache or from the provider if it is not yet cached. * Get the wrapped value, from cache or from the provider if it is not yet cached.
* *
* If the provider throws an exception, it will be redirected to the caller as is, and no value will be cached (the next call to this method will * If the provider throws an exception, it will be redirected to the caller as is, and no value will be cached
* execute the supplier again). * (the next call to this method will execute the supplier again).
*/ */
@Override
public synchronized T get() throws Exception { public synchronized T get() throws Exception {
if (!cached) // check outside synchronized method to reduce useless synchronization if value is already cached if (!cached)
set(supplier.get()); set(supplier.get());
return cachedValue; return cachedValue;
} }
/**
* Reset the cached value. The next call to {@link #get()} will get the value from the provider.
*/
public synchronized void reset() { public synchronized void reset() {
cached = false; cached = false;
cachedValue = null; cachedValue = null;
} }
/**
* Manually set the value to the provided one.
* @param value the value to put in the cache.
*/
public synchronized void set(T value) { public synchronized void set(T value) {
cachedValue = value; cachedValue = value;
cached = true; cached = true;

View File

@ -1,53 +1,144 @@
package fr.pandacube.lib.util; package fr.pandacube.lib.util;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
import java.util.function.ToIntBiFunction; import java.util.function.ToIntBiFunction;
/**
* Implementation of the <a href="https://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein distance algorithm</a>
* that operate on characters. Its purpose is to compute a "distance" between two strings of characters, that represents
* how many edition operation it is needed to perform on the first string ({@code initialString}) to optain the second
* one ({@code finalString}).
*
* All the parameters of the algorithm are configurable:
* <ul>
* <li>The score of adding a character</li>
* <li>The score of removing a character</li>
* <li>The score of replacing any pair of character</li>
* </ul>
*
* A simple usage of this class is to call the constructor {@link #LevenshteinDistance(String, String, int, int,
* ToIntBiFunction)} (for a full control of the parameters) or {@link #LevenshteinDistance(String, String)} (to keep the
* default parameters value); then to call the method {@link #getCurrentDistance()} to compute the Levenshtein distance
* between the two strings.
*
* A more advanced usage offer the possibility to progressively compute a distance from a predefined
* {@code initialString} to a {@code finalString} that is feeded progressively using {@link #add(char)} or
* {@link #add(String)}. This is useful if the {@code finalString} is an input that is currently being typed by the
* user, so the application can progressively update a list of suggested words based on the distance.
* For this usage, you can use those constructors to avoid initializing the {@code finalString}:
* {@link #LevenshteinDistance(String, int, int, ToIntBiFunction)} or {@link #LevenshteinDistance(String)}.
*
*
*/
public class LevenshteinDistance { public class LevenshteinDistance {
private final String initialList; private final String initialString;
private final int elementAdditionScore; private final int elementAdditionScore;
private final int elementDeletionScore; private final int elementDeletionScore;
private final ToIntBiFunction<Character, Character> elementDistanceFunction; private final ToIntBiFunction<Character, Character> elementDistanceFunction;
private int[] prev, curr; private int[] prev, curr; // dynamic programming interval arrays
private int progress = 0; private int progress = 0;
public LevenshteinDistance(String initList, String finList, int addScore, int delScore, ToIntBiFunction<Character, Character> elemDistFn) { /**
initialList = initList == null ? "" : initList; * Create a new instance of {@link LevenshteinDistance} that compute the edit-distance between {@code initialString}
elementAdditionScore = addScore; * and {@code finalString}.
elementDeletionScore = delScore; * <p>
elementDistanceFunction = elemDistFn == null ? ((e1, e2) -> Objects.equals(e1, e2) ? 0 : 1) : elemDistFn; * The score of each edition action is provided as parameters.
*
* @param initialString the initial string. Cannot be null.
* @param finalString the final string. Can be null, and may be provided later using {@link #add(String)} or
* character after character using {@link #add(char)}.
* @param elementAdditionScore the score for adding a character.
* @param elementDeletionScore the score for removing a character.
* @param elementDistanceFunction a {@link Function} that computes the score for replacing the character provided as
* first argument, to the character provided as second argument. If it is null, it
* will use a default function that will return 0 if the two characters are equals, 1
* otherwise.
*/
public LevenshteinDistance(String initialString, String finalString, int elementAdditionScore, int elementDeletionScore, ToIntBiFunction<Character, Character> elementDistanceFunction) {
this.initialString = Objects.requireNonNull(initialString, "initialList");
this.elementAdditionScore = elementAdditionScore;
this.elementDeletionScore = elementDeletionScore;
this.elementDistanceFunction = elementDistanceFunction == null ? ((e1, e2) -> Objects.equals(e1, e2) ? 0 : 1) : elementDistanceFunction;
prev = new int[initialList.length() + 1]; prev = new int[this.initialString.length() + 1];
curr = new int[initialList.length() + 1]; curr = new int[this.initialString.length() + 1];
for (int i = 0; i < curr.length; i++) for (int i = 0; i < curr.length; i++)
curr[i] = i * elementDeletionScore; curr[i] = i * this.elementDeletionScore;
add(finList); if (finalString != null)
add(finalString);
} }
public LevenshteinDistance() { /**
this(null, null, 1, 1, null); * Create a new instance of {@link LevenshteinDistance} that will compute the edit-distance between
* {@code initialString} and a final string provided later using {@link #add(String)} or character after character
* using {@link #add(char)}.
* <p>
* The score of each edition action is provided as parameters.
*
* @param initialString the initial string. Cannot be null.
* @param elementAdditionScore the score for adding a character.
* @param elementDeletionScore the score for removing a character.
* @param elementDistanceFunction a {@link Function} that computes the score for replacing the character provided as
* first argument, to the character provided as second argument. If it is null, it
* will use a default function that will return 0 if the two characters are equals, 1
* otherwise.
*/
public LevenshteinDistance(String initialString, int elementAdditionScore, int elementDeletionScore, ToIntBiFunction<Character, Character> elementDistanceFunction) {
this(initialString, null, elementAdditionScore, elementDeletionScore, elementDistanceFunction);
} }
public LevenshteinDistance(String initList) { /**
this(initList, null, 1, 1, null); * Create a new instance of {@link LevenshteinDistance} that compute the edit-distance between {@code initialString}
* and {@code finalString}.
* <p>
* All the edition action are valued to 1. To customize the edition action score, use
* {@link #LevenshteinDistance(String, String, int, int, ToIntBiFunction)}.
*
* @param initialString the initial string. Cannot be null.
* @param finalString the final string. Can be null, and may be provided later using {@link #add(String)} or
* character after character using {@link #add(char)}.
*/
public LevenshteinDistance(String initialString, String finalString) {
this(initialString, finalString, 1, 1, null);
} }
public int getCurrentDistance() { /**
return curr[curr.length - 1]; * Create a new instance of {@link LevenshteinDistance} that will compute the edit-distance between
* {@code initialString} and a final string provided later using {@link #add(String)} or character after character
* using {@link #add(char)}.
* <p>
* All the edition action are valued to 1. To customize the edition action score, use
* {@link #LevenshteinDistance(String, int, int, ToIntBiFunction)}.
*
* @param initialString the initial string. Cannot be null.
*/
public LevenshteinDistance(String initialString) {
this(initialString, null);
} }
/**
* Add the provided string of characters to the {@code finalString}, and update the Levenshtein distance returned by
* {@link #getCurrentDistance()}.
* @param els the string of character.
*/
public void add(String els) { public void add(String els) {
for (char el : els.toCharArray()) for (char el : els.toCharArray())
add(el); add(el);
} }
/**
* Add a single character to the {@code finalString}, and update the Levenshtein distance returned by
* * {@link #getCurrentDistance()}.
* @param el the character.
*/
public void add(char el) { public void add(char el) {
progress++; progress++;
// swap score arrays // swap score arrays
@ -56,13 +147,20 @@ public class LevenshteinDistance {
curr[0] = progress * elementAdditionScore; curr[0] = progress * elementAdditionScore;
for (int i = 1; i < curr.length; i++) { for (int i = 1; i < curr.length; i++) {
int S = prev[i - 1] + elementDistanceFunction.applyAsInt(initialList.charAt(i - 1), el); int S = prev[i - 1] + elementDistanceFunction.applyAsInt(initialString.charAt(i - 1), el);
int A = prev[i] + elementAdditionScore; int A = prev[i] + elementAdditionScore;
int D = curr[i - 1] + elementDeletionScore; int D = curr[i - 1] + elementDeletionScore;
curr[i] = Math.min(S, Math.min(A, D)); curr[i] = Math.min(S, Math.min(A, D));
} }
} }
/**
* Get the currently calculated Levenshtein distance from the {@code initialString} to the already provided
* {@code finalString}.
* @return the Levenshtein distance.
*/
public int getCurrentDistance() {
return curr[curr.length - 1];
}
} }

View File

@ -2,9 +2,20 @@ package fr.pandacube.lib.util;
import java.util.List; import java.util.List;
/**
* Provides utility methods related to lists.
*/
public class ListUtil { public class ListUtil {
/**
* Utility method to add to the provided list all the values in the provided interval.
* <p>
* If {@code min > max}, the list is not modified.
*
* @param list the list to add the values in.
* @param min the inclusive min value.
* @param max the inclusive max value.
*/
public static void addLongRangeToList(List<Long> list, long min, long max) { public static void addLongRangeToList(List<Long> list, long min, long max) {
for (long i = min; i <= max; i++) { for (long i = min; i <= max; i++) {
list.add(i); list.add(i);

View File

@ -4,65 +4,156 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
/**
* Utility class to easily log info into a provided logger. This class avoid the needs to fetch the logger everytime it
* is needed.
*
* For instance, this piece of code:
* <pre>
* getTheLoggerFromSomewhere().info(message);
* </pre>
*
* Can be simplified by one line to put in the startup code of the application:
* <pre>
* Log.setLogger(getTheLoggerFromSomewhere());
* </pre>
* And this code everywhere the application needs to log something:
* <pre>
* Log.info(message);
* </pre>
*
* This the {@link #setLogger(Logger)} method is not called, thi class will use the logger returned by
* {@link Logger#getGlobal()}.
*/
public final class Log { public final class Log {
private static Logger logger = Logger.getGlobal(); private static Logger logger = Logger.getGlobal();
private static final AtomicBoolean logDebug = new AtomicBoolean(false); private static final AtomicBoolean logDebug = new AtomicBoolean(false);
public static void setDebug(boolean newVal) { /**
logDebug.set(newVal); * Determine if {@link #debug(Throwable)}, {@link #debug(String)} and {@link #debug(String, Throwable)} will actually
* log a message or not.
* @param debug true to enable debug, false otherwise
*/
public static void setDebug(boolean debug) {
logDebug.set(debug);
} }
/**
* Tells if the debug mode is enabled or not.
* @return true if debug is enabled, false otherwise.
*/
public static boolean isDebugEnabled() { public static boolean isDebugEnabled() {
return logDebug.get(); return logDebug.get();
} }
/**
* Get the backend logger of this class.
* @return the logger.
*/
public static Logger getLogger() { public static Logger getLogger() {
return logger; return logger;
} }
public static void setLogger(Logger l) { /**
logger = l; * Set the backend logger of this class.
* @param logger the logger to use.
*/
public static void setLogger(Logger logger) {
Log.logger = logger;
} }
/**
* Log the provided message using logger level {@link Level#INFO}.
* @param message the message to log
* @see Logger#info(String)
*/
public static void info(String message) { public static void info(String message) {
logger.info(message); logger.info(message);
} }
public static void warning(String message, Throwable t) { /**
logger.log(Level.WARNING, message, t); * Log the provided message and throwable using logger level {@link Level#WARNING}.
* @param message the message to log
* @param throwable the throwable to log
*/
public static void warning(String message, Throwable throwable) {
logger.log(Level.WARNING, message, throwable);
} }
public static void warning(Throwable t) { /**
logger.log(Level.WARNING, "", t); * Log the provided throwable using logger level {@link Level#WARNING}.
* @param throwable the throwable to log
*/
public static void warning(Throwable throwable) {
logger.log(Level.WARNING, "", throwable);
} }
/**
* Log the provided message using logger level {@link Level#WARNING}.
* @param message the message to log
* @see Logger#warning(String)
*/
public static void warning(String message) { public static void warning(String message) {
logger.warning(message); logger.warning(message);
} }
public static void severe(String message, Throwable t) { /**
logger.log(Level.SEVERE, message, t); * Log the provided message and throwable using logger level {@link Level#SEVERE}.
* @param message the message to log
* @param throwable the throwable to log
*/
public static void severe(String message, Throwable throwable) {
logger.log(Level.SEVERE, message, throwable);
} }
public static void severe(Throwable t) { /**
logger.log(Level.SEVERE, "", t); * Log the provided throwable using logger level {@link Level#SEVERE}.
* @param throwable the throwable to log
*/
public static void severe(Throwable throwable) {
logger.log(Level.SEVERE, "", throwable);
} }
/**
* Log the provided message using logger level {@link Level#SEVERE}.
* @param message the message to log
* @see Logger#severe(String)
*/
public static void severe(String message) { public static void severe(String message) {
logger.severe(message); logger.severe(message);
} }
public static void debug(String message, Throwable t) { /**
* Log the provided message and throwable using logger level {@link Level#INFO}, if the debug mode is enabled.
* @param message the message to log
* @param throwable the throwable to log
* @see #isDebugEnabled()
* @see #setDebug(boolean)
*/
public static void debug(String message, Throwable throwable) {
if (!logDebug.get()) return; if (!logDebug.get()) return;
logger.log(Level.INFO, message, t); logger.log(Level.INFO, message, throwable);
} }
public static void debug(Throwable t) { /**
* Log the provided throwable using logger level {@link Level#INFO}, if the debug mode is enabled.
* @param throwable the throwable to log
* @see #isDebugEnabled()
* @see #setDebug(boolean)
*/
public static void debug(Throwable throwable) {
if (!logDebug.get()) return; if (!logDebug.get()) return;
logger.log(Level.INFO, "", t); logger.log(Level.INFO, "", throwable);
} }
/**
* Log the provided message using logger level {@link Level#INFO}, if the debug mode is enabled.
* @param message the message to log
* @see #isDebugEnabled()
* @see #setDebug(boolean)
* @see Logger#info(String)
*/
public static void debug(String message) { public static void debug(String message) {
if (!logDebug.get()) return; if (!logDebug.get()) return;
logger.info(message); logger.info(message);

View File

@ -7,29 +7,31 @@ import java.util.ListIterator;
import java.util.function.Function; import java.util.function.Function;
/** /**
* A Wrapper list that provides a mapped view of the backend list. * A Wrapper list that provides a mapped view of the backend list. Every modification of this list will modify the bakend
* Every modification of this list will modify the bakend list. * list. Each time a value is accessed or modified, the appropriate setter or getter is used to convert the value
* For each time a value is accessed or modified, the appropriate * between the source {@code S} and the visible {@code T} type.
* setter or getter is used to convert the value between the source {@code S}
* and the visible {@code T} type.
* @param <S> the source (backend) type * @param <S> the source (backend) type
* @param <T> the visible (mapped) type * @param <T> the visible (mapped) type
*/ */
public class MappedListView<S, T> extends AbstractList<T> { public class MappedListView<S, T> extends AbstractList<T> {
/**
* The backend list of this {@link MappedListView}.
*/
protected final List<S> backend; protected final List<S> backend;
private final Function<S, T> getter; private final Function<S, T> getter;
private final Function<T, S> setter; private final Function<T, S> setter;
/** /**
* * Create a new wrapper for the provided backend list.
* @param backend the list backing this list * @param backend the list backing this list
* @param getter the function converting the value from the backing list to the value of this list. * @param getter the function converting the value from the backing list to the value of this list. It is used for
* It is used for every operation involving reading data from the backing list and comparing existing data. * every operation involving reading data from the backing list and comparing existing data. For
* For instance, {@link #indexOf(Object)} iterate through the backing list, converting all the values * instance, {@link #indexOf(Object)} iterate through the backing list, converting all the values with
* with {@code getter} before comparing them with the parameter of {@link #indexOf(Object)}. * {@code getter} before comparing them with the parameter of {@link #indexOf(Object)} before
* before comparing * comparing.
* @param setter used for modification of the data in the list (typically {@code add} and {@code set} methods) * @param setter used for modification of the data in the list (typically {@link #add(Object)} and
* {@link #set(int, Object)} methods)
*/ */
public MappedListView(List<S> backend, Function<S, T> getter, Function<T, S> setter) { public MappedListView(List<S> backend, Function<S, T> getter, Function<T, S> setter) {
this.backend = backend; this.backend = backend;
@ -37,75 +39,115 @@ public class MappedListView<S, T> extends AbstractList<T> {
this.setter = setter; this.setter = setter;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public int size() { public int size() {
return backend.size(); return backend.size();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public T get(int index) { public T get(int index) {
return getter.apply(backend.get(index)); return getter.apply(backend.get(index));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public T set(int index, T element) { public T set(int index, T element) {
return getter.apply(backend.set(index, setter.apply(element))); return getter.apply(backend.set(index, setter.apply(element)));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean add(T element) { public boolean add(T element) {
return backend.add(setter.apply(element)); return backend.add(setter.apply(element));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void add(int index, T element) { public void add(int index, T element) {
backend.add(index, setter.apply(element)); backend.add(index, setter.apply(element));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public T remove(int index) { public T remove(int index) {
return getter.apply(backend.remove(index)); return getter.apply(backend.remove(index));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void clear() { public void clear() {
backend.clear(); backend.clear();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public List<T> subList(int fromIndex, int toIndex) { public List<T> subList(int fromIndex, int toIndex) {
return new MappedListView<>(backend.subList(fromIndex, toIndex), getter, setter); return new MappedListView<>(backend.subList(fromIndex, toIndex), getter, setter);
} }
/**
* {@inheritDoc}
*/
@Override @Override
protected void removeRange(int fromIndex, int toIndex) { protected void removeRange(int fromIndex, int toIndex) {
backend.subList(fromIndex, toIndex).clear(); backend.subList(fromIndex, toIndex).clear();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
return backend.equals(o); return backend.equals(o);
} }
/**
* {@inheritDoc}
*/
@Override @Override
public int hashCode() { public int hashCode() {
return backend.hashCode(); return backend.hashCode();
} }
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public int indexOf(Object o) { public int indexOf(Object o) {
return backend.indexOf(setter.apply((T) o)); return backend.indexOf(setter.apply((T) o));
} }
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public int lastIndexOf(Object o) { public int lastIndexOf(Object o) {
return backend.lastIndexOf(setter.apply((T) o)); return backend.lastIndexOf(setter.apply((T) o));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public Iterator<T> iterator() { public Iterator<T> iterator() {
return new Iterator<>() { return new Iterator<>() {
@ -127,11 +169,17 @@ public class MappedListView<S, T> extends AbstractList<T> {
}; };
} }
/**
* {@inheritDoc}
*/
@Override @Override
public ListIterator<T> listIterator() { public ListIterator<T> listIterator() {
return listIterator(0); return listIterator(0);
} }
/**
* {@inheritDoc}
*/
@Override @Override
public ListIterator<T> listIterator(int index) { public ListIterator<T> listIterator(int index) {
return new ListIterator<>() { return new ListIterator<>() {

View File

@ -2,48 +2,28 @@ package fr.pandacube.lib.util;
import java.text.DecimalFormat; import java.text.DecimalFormat;
/**
* This class contains various methods to manipulate and display memory measurements.
*/
public class MemoryUtil { public class MemoryUtil {
public enum MemoryUnit {
B(1, 1, null),
KB(1024, 1000, "k"),
MB(1024 * 1024, 1000_000, "M"),
GB(1024 * 1024 * 1024, 1000_000_000, "G");
final long valueTrad;
final long valueSI;
final String unitMultiplier;
public long toUnitRound(long byteCount, boolean si) {
return byteCount / value(si);
}
public double toUnit(long byteCount, boolean si) {
return byteCount / (double)value(si);
}
public long value(boolean si) {
return si ? valueSI : valueTrad;
}
public String unit(boolean si) {
return unitMultiplier == null ? "o" : (unitMultiplier + (si ? "o" : "io"));
}
MemoryUnit(long vTrad, long vSI, String uMult) {
valueTrad = vTrad;
valueSI = vSI;
unitMultiplier = uMult;
}
}
private static final DecimalFormat format = new DecimalFormat("#####0.00"); private static final DecimalFormat format = new DecimalFormat("#####0.00");
public static String humanReadableSize(long octet, MemoryUnit roundTo, boolean si) { /**
* Generate a string representation of the provided memory amount, using the provided memory unit and either to use
* SI or traditional units.
* <p>
* <b>This method returns the unit symbol in French.</b>
* @param bytes the memory amount to format.
* @param roundTo the memory unit to convert and display the amount to.
* @param si true to use SI unit, false to use traditional.
* @return a string representation of the provided memory amount.
*/
public static String humanReadableSize(long bytes, MemoryUnit roundTo, boolean si) {
boolean neg = octet < 0; boolean neg = bytes < 0;
long size = Math.abs(octet); long size = Math.abs(bytes);
MemoryUnit unit = roundTo; MemoryUnit unit = roundTo;
for (int ui = MemoryUnit.values().length - 1; ui >= 0; ui--) { for (int ui = MemoryUnit.values().length - 1; ui >= 0; ui--) {
@ -58,7 +38,7 @@ public class MemoryUtil {
String dispValue; String dispValue;
if (unit == roundTo) { if (unit == roundTo) {
dispValue = ""+unit.toUnitRound(size, si); dispValue = "" + unit.toUnitRound(size, si);
} }
else { else {
dispValue = format.format(unit.toUnit(size, si)); dispValue = format.format(unit.toUnit(size, si));
@ -67,8 +47,116 @@ public class MemoryUtil {
return (neg ? "-" : "") + dispValue + unit.unit(si); return (neg ? "-" : "") + dispValue + unit.unit(si);
} }
public static String humanReadableSize(long octet) { /**
return humanReadableSize(octet, MemoryUnit.B, false); * Generate a string representation of the provided memory amount, displayinh the value in byte (as is) and with the
* unit symbol {@code "o"}.
* <p>
* <b>This method returns the unit symbol in French.</b>
* @param bytes the memory amount to format.
* @return a string representation of the provided memory amount.
*/
public static String humanReadableSize(long bytes) {
return humanReadableSize(bytes, MemoryUnit.B, false);
}
/**
* Enumeration of comonly used unit of memory prefix.
*/
public enum MemoryUnit {
/**
* Byte unit.
* <p>
* SI unit: 10<sup>0</sup> = 1B<br>
* Traditional unit: 2<sup>0</sup> = 1B
*/
B(1, 1, null),
/**
* Kilobyte unit.
* <p>
* SI unit: 10<sup>3</sup>B = 1000B = 1KB (SI unit)<br>
* Traditional unit: 2<sup>10</sup> = 1024B = 1KiB
*/
KB(1024, 1000, "k"),
/**
* Megabyte unit.
* <p>
* SI unit: 10<sup>6</sup>B = 1000000B = 1MB (SI unit)<br>
* Traditional unit: 2<sup>20</sup> = 1048576B = 1MiB
*/
MB(1024 * 1024, 1000_000, "M"),
/**
* Gigabyte unit.
* <p>
* SI unit: 10<sup>9</sup>B = 1000000000B = 1GB (SI unit)<br>
* Traditional unit: 2<sup>30</sup> = 1073741824B = 1GiB
*/
GB(1024 * 1024 * 1024, 1000_000_000, "G");
/**
* The traditional (power of 2) value of this memory unit, in byte.
*/
public final long valueTrad;
/**
* The SI standard (power of 10) value of this memory unit, in byte.
*/
public final long valueSI;
/**
* The prefix symbol for this unit.
*/
public final String unitMultiplier;
/**
* Converts the provided memory amount to this unit, rounded down (using integer division).
* @param byteCount the memory amount to convert.
* @param si true to use SI unit, false to use traditional.
* @return the converted value.
*/
public long toUnitRound(long byteCount, boolean si) {
return byteCount / value(si);
}
/**
* Converts the provided memory amount to this unit.
* @param byteCount the memory amount to convert.
* @param si true to use SI unit, false to use traditional.
* @return the converted value.
*/
public double toUnit(long byteCount, boolean si) {
return byteCount / (double)value(si);
}
/**
* The value of this memory unit, in byte, in either SI or traditional unit.
* @param si true to use SI unit, false to use traditional.
* @return value of this memory unit, in byte.
*/
public long value(boolean si) {
return si ? valueSI : valueTrad;
}
/**
* Returns the full unit symbol of this unit, that is the prefix {@link #unitMultiplier} concatenated with
* either "o" for SI unit or "io" to traditional unit.
* @param si true to use SI unit, false to use traditional.
* @return the full unit symbol of this unit.
*/
public String unit(boolean si) {
return unitMultiplier == null ? "o" : (unitMultiplier + (si ? "o" : "io"));
}
MemoryUnit(long vTrad, long vSI, String uMult) {
valueTrad = vTrad;
valueSI = vSI;
unitMultiplier = uMult;
}
} }
} }

View File

@ -8,52 +8,96 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
/**
* Enumeration of all known, post Netty-rewrite (1.7.2+), stable Minecraft Java versions.
* <p>
* It provides various utility methods to nicely display a set of Minecraft version (for instance "1.13.x",
* "1.16-1.16.3", "1.8.x and 1.9", "1.18.1 or 1.18.2")
* <p>
* Note that this enum uses one value to represent every Minecraft version using the same protocol version number.
*/
public enum MinecraftVersion { public enum MinecraftVersion {
/** Minecraft versions 1.7.2 to 1.7.5, protocol version 4. */
v1_7_2_to_1_7_5(4, "1.7.2-1.7.5"), v1_7_2_to_1_7_5(4, "1.7.2-1.7.5"),
/** Minecraft versions 1.7.6 to 1.7.10, protocol version 5. */
v1_7_6_to_1_7_10(5, "1.7.6-1.7.10"), v1_7_6_to_1_7_10(5, "1.7.6-1.7.10"),
/** Minecraft versions 1.8.x, protocol version 47. */
v1_8(47, "1.8.x"), v1_8(47, "1.8.x"),
/** Minecraft version 1.9, protocol version 107. */
v1_9(107, "1.9"), v1_9(107, "1.9"),
/** Minecraft version 1.9.1, protocol version 108. */
v1_9_1(108, "1.9.1"), v1_9_1(108, "1.9.1"),
/** Minecraft version 1.9.2, protocol version 109. */
v1_9_2(109, "1.9.2"), v1_9_2(109, "1.9.2"),
/** Minecraft versions 1.9.3 and 1.9.4, protocol version 110. */
v1_9_3_to_1_9_4(110, "1.9.3", "1.9.4"), v1_9_3_to_1_9_4(110, "1.9.3", "1.9.4"),
/** Minecraft versions 1.10.x, protocol version 210. */
v1_10(210, "1.10.x"), v1_10(210, "1.10.x"),
/** Minecraft version 1.11, protocol version 315. */
v1_11(315, "1.11"), v1_11(315, "1.11"),
/** Minecraft versions 1.11.1 and 1.11.2, protocol version 316. */
v1_11_1_to_1_11_2(316, "1.11.1", "1.11.2"), v1_11_1_to_1_11_2(316, "1.11.1", "1.11.2"),
/** Minecraft version 1.12, protocol version 335. */
v1_12(335, "1.12"), v1_12(335, "1.12"),
/** Minecraft version 1.12.1, protocol version 338. */
v1_12_1(338, "1.12.1"), v1_12_1(338, "1.12.1"),
/** Minecraft version 1.12.2, protocol version 340. */
v1_12_2(340, "1.12.2"), v1_12_2(340, "1.12.2"),
/** Minecraft version 1.13, protocol version 393. */
v1_13(393, "1.13"), v1_13(393, "1.13"),
/** Minecraft version 1.13.1, protocol version 401. */
v1_13_1(401, "1.13.1"), v1_13_1(401, "1.13.1"),
/** Minecraft version 1.13.2, protocol version 404. */
v1_13_2(404, "1.13.2"), v1_13_2(404, "1.13.2"),
/** Minecraft version 1.14, protocol version 477. */
v1_14(477, "1.14"), v1_14(477, "1.14"),
/** Minecraft version 1.14.1, protocol version 480. */
v1_14_1(480, "1.14.1"), v1_14_1(480, "1.14.1"),
/** Minecraft version 1.14.2, protocol version 485. */
v1_14_2(485, "1.14.2"), v1_14_2(485, "1.14.2"),
/** Minecraft version 1.14.3, protocol version 490. */
v1_14_3(490, "1.14.3"), v1_14_3(490, "1.14.3"),
/** Minecraft version 1.14.4, protocol version 498. */
v1_14_4(498, "1.14.4"), v1_14_4(498, "1.14.4"),
/** Minecraft version 1.15, protocol version 573. */
v1_15(573, "1.15"), v1_15(573, "1.15"),
/** Minecraft version 1.15.1, protocol version 575. */
v1_15_1(575, "1.15.1"), v1_15_1(575, "1.15.1"),
/** Minecraft version 1.15.2, protocol version 578. */
v1_15_2(578, "1.15.2"), v1_15_2(578, "1.15.2"),
/** Minecraft version 1.16, protocol version 735. */
v1_16(735, "1.16"), v1_16(735, "1.16"),
/** Minecraft version 1.16.1, protocol version 736. */
v1_16_1(736, "1.16.1"), v1_16_1(736, "1.16.1"),
/** Minecraft version 1.16.2, protocol version 751. */
v1_16_2(751, "1.16.2"), v1_16_2(751, "1.16.2"),
/** Minecraft version 1.16.3, protocol version 753. */
v1_16_3(753, "1.16.3"), v1_16_3(753, "1.16.3"),
/** Minecraft versions 1.16.4 and 1.16.5, protocol version 754. */
v1_16_4_to_1_16_5(754, "1.16.4", "1.16.5"), v1_16_4_to_1_16_5(754, "1.16.4", "1.16.5"),
/** Minecraft version 1.17, protocol version 755. */
v1_17(755, "1.17"), v1_17(755, "1.17"),
/** Minecraft version 1.17.1, protocol version 756. */
v1_17_1(756, "1.17.1"), v1_17_1(756, "1.17.1"),
/** Minecraft versions 1.18 and 1.18.1, protocol version 757. */
v1_18_to_1_18_1(757, "1.18", "1.18.1"), v1_18_to_1_18_1(757, "1.18", "1.18.1"),
/** Minecraft version 1.18.2, protocol version 758. */
v1_18_2(758, "1.18.2"), v1_18_2(758, "1.18.2"),
/** Minecraft version 1.19, protocol version 759. */
v1_19(759, "1.19"), v1_19(759, "1.19"),
/** Minecraft version 1.19.1, protocol version 759. */
v1_19_1(760, "1.19.1"); v1_19_1(760, "1.19.1");
// IMPORTANT: don't forget to update the versionMergeDisplay value when adding a new version; // IMPORTANT: don't forget to update the versionMergeDisplay value when adding a new version;
@ -156,45 +200,144 @@ public enum MinecraftVersion {
} }
public final int id; /**
public final List<String> versionDisplay; * The protocol version number of this Minecraft version.
*/
public final int protocolVersionNumber;
/**
* All Minecraft version supported by this protocol version number.
*/
public final List<String> versionsDisplay;
MinecraftVersion(int v, String... d) { MinecraftVersion(int protocolVersionNumber, String... versionsDisplay) {
id = v; this.protocolVersionNumber = protocolVersionNumber;
versionDisplay = Arrays.asList(d); this.versionsDisplay = Arrays.asList(versionsDisplay);
} }
@Override @Override
public String toString() { public String toString() {
return toStringAnd(); return name() + "{protocol=" + protocolVersionNumber + ", toString(\"and\")=" + toString("and") + "}";
} }
/**
* Returns a string representation of all the Minecraft version of this enum value, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
*
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
* "or" or any other word of any language. The spaces before and after are already
* concatenated.
* @return a string representation of this {@link MinecraftVersion}.
*/
public String toString(String finalWordSeparator) {
return StringUtil.joinGrammatically(", ", " " + finalWordSeparator + " ", versionsDisplay);
}
/**
* Returns a string representation of all the Minecraft version of this enum value, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)} with the gramatical word "et"
* ("and" in french).
*
* @return a string representation of this {@link MinecraftVersion}.
* @deprecated it uses the hardcoded french word "et" as the final word separator.
* Use {@link #displayOptimizedListOfVersions(List, String)} with "et" as the last parameter instead.
*/
@Deprecated
public String toStringAnd() { public String toStringAnd() {
return StringUtil.joinGrammatically(", ", " et ", versionDisplay); return toString("et");
} }
/**
* Returns a string representation of all the Minecraft version of this enum value, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)} with the gramatical word "ou"
* ("or" in french).
*
* @return a string representation of this {@link MinecraftVersion}.
* @deprecated it uses the hardcoded french word "ou" as the final word separator.
* Use {@link #displayOptimizedListOfVersions(List, String)} with "ou" as the last parameter instead.
*/
@Deprecated
public String toStringOr() { public String toStringOr() {
return StringUtil.joinGrammatically(", ", " ou ", versionDisplay); return toString("ou");
} }
public static MinecraftVersion getVersion(int v) {
for (MinecraftVersion mcV : values())
if (mcV.id == v) return mcV;
/**
* Gets the {@link MinecraftVersion} instance associated with the provided protocol version number.
*
* @param protocolVersionNumber the protocol version number
* @return the {@link MinecraftVersion} instance associated with the provided protocol version number, or null if
* there is none.
*/
public static MinecraftVersion getVersion(int protocolVersionNumber) {
for (MinecraftVersion mcV : values())
if (mcV.protocolVersionNumber == protocolVersionNumber) return mcV;
return null; return null;
} }
/**
* Generate a string representation of the provided list of version, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
*
* @param versions the minecraft versions to list
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
* "or" or any other word of any language. The spaces before and after are already
* concatenated.
* @return a string representation of the provided list of version.
*/
public static String displayOptimizedListOfVersions(List<MinecraftVersion> versions, String finalWordSeparator) {
return StringUtil.joinGrammatically(", ", " " + finalWordSeparator + " ", getVersionsDisplayList(versions));
}
/**
* Generate a string representation of the provided list of version, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)} with the gramatical word "et"
* ("and" in french).
*
* @param versions the minecraft versions to list
* @return a string representation of the provided list of version.
* @deprecated it uses the hardcoded french word "et" as the final word separator.
* Use {@link #displayOptimizedListOfVersions(List, String)} with "et" as the last parameter instead.
*/
@Deprecated
public static String displayOptimizedListOfVersionsAnd(List<MinecraftVersion> versions) { public static String displayOptimizedListOfVersionsAnd(List<MinecraftVersion> versions) {
return StringUtil.joinGrammatically(", ", " et ", getVersionsDisplayList(versions)); return displayOptimizedListOfVersions(versions, "et");
} }
/**
* Generate a string representation of the provided list of version, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)} with the gramatical word "ou"
* ("or" in french).
*
* @param versions the minecraft versions to list
* @return a string representation of the provided list of version.
* @deprecated it uses the hardcoded french word "ou" as the final word separator.
* Use {@link #displayOptimizedListOfVersions(List, String)} with "ou" as the last parameter instead.
*/
@Deprecated
public static String displayOptimizedListOfVersionsOr(List<MinecraftVersion> versions) { public static String displayOptimizedListOfVersionsOr(List<MinecraftVersion> versions) {
return StringUtil.joinGrammatically(", ", " ou ", getVersionsDisplayList(versions)); return displayOptimizedListOfVersions(versions, "ou");
} }
/**
* Returns an optimized list of string representation of Minecraft version, that represent the provided list of
* Minecraft version.
* <p>
* This methods try to merge successive Minecraft version into a single string: for instance, all versions from 1.18
* to 1.18.2 are represented by the string "1.18.x"; all version from 1.14.1 to 1.14.4 are represented by the string
* "1.14.1-1.14.4".
* <p>
* All possible merges of {@link MinecraftVersion} are listed in the static initializer of this enum.
*
* @param vList the {@link List} of {@link MinecraftVersion}
* @return an optimized list of string representation of Minecraft version.
*/
public static List<String> getVersionsDisplayList(List<MinecraftVersion> vList) { public static List<String> getVersionsDisplayList(List<MinecraftVersion> vList) {
if (vList == null) if (vList == null)
return new ArrayList<>(); return new ArrayList<>();
@ -218,7 +361,7 @@ public enum MinecraftVersion {
} }
if (vSubSet.size() == 1) { if (vSubSet.size() == 1) {
ret.addAll(values()[i].versionDisplay); ret.addAll(values()[i].versionsDisplay);
} }
else { else {
ret.addAll(versionMergeDisplay.get(vSubSet)); ret.addAll(versionMergeDisplay.get(vSubSet));

View File

@ -1,16 +1,26 @@
package fr.pandacube.lib.util; package fr.pandacube.lib.util;
/**
* Provides utility methods around Minecraft and Web stuff.
*/
public class MinecraftWebUtil { public class MinecraftWebUtil {
/** /**
Convert a legacy Minecraft color coded String into HTML Format. * Convert a legacy Minecraft color coded String into HTML Format.
* <p>
* Each colored part of the text will be contained in a {@code <span>} tag with {@code class="cX"} where {@code X}
* is the color code from 0 to F in uppercase.
* The bold, striked, underlined and italic parts will be contained in a {@code <span>} tag with respectively the
* classes {@code cL}, {@code cM}, {@code cN} and {@code cO}.
* Some CSS properties are needed to apply the colors to the CSS classes.
* @param chatcolorPrefix the prefix used for the color codes
* @param legacyText the legacy text to convert to HTML.
* @return The text formated in HTML.
* @implNote the current implementation does not yet supports the RGB colors.
*/ */
// TODO update to support RGB colors (Bungee and Essentials notation). // TODO update to support RGB colors (Bungee and Essentials notation). (see JS implementation at https://www.pandacube.fr/assets/js/global.js )
// See JavaScript implementation at https://www.pandacube.fr/assets/js/global.js // TODO moves this to pandalib-chat and use Adventure API to help serializing to HTML
public static String fromMinecraftColorCodeToHTML(char code_prefix, String ligne) public static String fromMinecraftColorCodeToHTML(char chatcolorPrefix, String legacyText)
{ {
String color_char = "0123456789abcdefr"; String color_char = "0123456789abcdefr";
@ -18,12 +28,12 @@ public class MinecraftWebUtil {
char currentColor = 'r'; char currentColor = 'r';
boolean bold = false, italic = false, underlined = false, strikethrough = false; boolean bold = false, italic = false, underlined = false, strikethrough = false;
for (int i=0; i<ligne.length(); i++) { for (int i=0; i<legacyText.length(); i++) {
char c = ligne.charAt(i); char c = legacyText.charAt(i);
if (c == code_prefix && (i<ligne.length()-1)) { if (c == chatcolorPrefix && (i<legacyText.length()-1)) {
i++; i++;
c = ligne.charAt(i); c = legacyText.charAt(i);
if (color_char.contains(String.valueOf(Character.toLowerCase(c)))) { if (color_char.contains(String.valueOf(Character.toLowerCase(c)))) {
if (bold) { if (bold) {
builder.append("</span>"); builder.append("</span>");
@ -75,7 +85,7 @@ public class MinecraftWebUtil {
} }
} }
else { else {
builder.append(code_prefix + c); builder.append(chatcolorPrefix + c);
} }

View File

@ -5,13 +5,37 @@ import java.util.Objects;
import java.util.Scanner; import java.util.Scanner;
import java.util.UUID; import java.util.UUID;
/**
* Utility class and program that generate offline UUIDs for provided player names.
* <p>
* You can generate the UUID programatically using {@link #getFromNickName(String)} and
* {@link #getFromNickNames(String[])}.
*
* To use this class as a program, type
* <pre>
* java -cp&lt;anyClassPathContainingThisClass&gt; fr.pandacube.lib.util.OfflineUUID [playernames...]
* </pre>
* Each argument will be interpreted as a player name. If there is no argument, the program will wait for them in the
* input stream.
* For each player name, the program will print the player name, a {@code tab} character, the UUID and a line separator.
*/
public class OfflineUUID { public class OfflineUUID {
/**
* Generate the offline {@link UUID} of the provided player name.
* @param nickname the player name to optain the offline UUID from.
* @return the offline {@link UUID} of the provided player name.
*/
public static UUID getFromNickName(String nickname) { public static UUID getFromNickName(String nickname) {
byte[] from_str = ("OfflinePlayer:" + nickname).getBytes(StandardCharsets.UTF_8); byte[] from_str = ("OfflinePlayer:" + nickname).getBytes(StandardCharsets.UTF_8);
return UUID.nameUUIDFromBytes(from_str); return UUID.nameUUIDFromBytes(from_str);
} }
/**
* Generate the offline {@link UUID}s of the provided player names.
* @param nicknames an array of player name to optain the offline UUIDs from.
* @return the offline {@link UUID}s of the provided player name in an array, at the same order as the input.
*/
public static UUID[] getFromNickNames(String[] nicknames) { public static UUID[] getFromNickNames(String[] nicknames) {
Objects.requireNonNull(nicknames); Objects.requireNonNull(nicknames);
@ -22,24 +46,25 @@ public class OfflineUUID {
} }
/**
* Main method for this class.
* @param args the arguments. One argument is one player name.
*/
public static void main(String[] args) { public static void main(String[] args) {
if (args.length == 0) { if (args.length == 0) {
try (Scanner s = new Scanner(System.in)) { try (Scanner s = new Scanner(System.in)) {
for(;;) { for(;;) {
System.out.print("Please input a player name: "); System.err.print("Please input a player name: ");
if (!s.hasNext()) if (!s.hasNextLine())
break; break;
String line = s.nextLine(); String line = s.nextLine();
System.out.println(getFromNickName(line)); System.out.println(line + "\t" + getFromNickName(line));
} }
} }
} }
else { else {
for (String arg : args) for (String arg : args)
System.out.println("" + arg + ":" + getFromNickName(arg)); System.out.println(arg + "\t" + getFromNickName(arg));
} }
} }
} }

View File

@ -3,67 +3,118 @@ package fr.pandacube.lib.util;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.random.RandomGenerator;
/**
* Utility class to generate random things.
*/
public class RandomUtil { public class RandomUtil {
/**
* The unique {@link Random} instance used in this class. Can also be used else where when needed.
*/
public static final Random rand = new Random(); public static final Random rand = new Random();
/**
* Returns a randomly generated integer between {@code minInclu} included and {@code maxExclu} excluded.
* @param minInclu the minimum value, included.
* @param maxExclu the maximum value, excluded.
* @return a random number between {@code minInclu} included and {@code maxExclu} excluded.
* @see Random#nextInt(int, int)
* @throws IllegalArgumentException if {@code minInclu} is greater than {@code maxExclu}.
* @deprecated Use {@link Random#nextInt(int, int)} instead.
*/
@Deprecated(forRemoval = true)
public static int nextIntBetween(int minInclu, int maxExclu) { public static int nextIntBetween(int minInclu, int maxExclu) {
return rand.nextInt(maxExclu - minInclu) + minInclu; return rand.nextInt(minInclu, maxExclu);
}
public static double nextDoubleBetween(double minInclu, double maxExclu) {
return rand.nextDouble() * (maxExclu - minInclu) + minInclu;
}
public static <T> T arrayElement(T[] arr) {
return (arr == null || arr.length == 0) ? null : arr[rand.nextInt(arr.length)];
}
public static <T> T listElement(List<T> arr) {
return (arr == null || arr.isEmpty()) ? null : arr.get(rand.nextInt(arr.size()));
}
public static char stringChar(String arr) {
return (arr == null || arr.isEmpty()) ? '\0' : arr.charAt(rand.nextInt(arr.length()));
} }
/** /**
* Returns a random value from a set. * Returns a randomly generated double between {@code minInclu} included and {@code maxExclu} excluded.
* * @param minInclu the minimum value, included.
* May not be optimized (Actually O(n) ) * @param maxExclu the maximum value, excluded.
* @param set the Set from which to pick a random value * @return a random number between {@code minInclu} included and {@code maxExclu} excluded.
* @return a random value from the set * @see Random#nextDouble(double, double)
* @throws IllegalArgumentException if {@code minInclu} is greater than {@code maxExclu}.
* @deprecated Use {@link Random#nextDouble(double, double)} instead.
*/
@Deprecated(forRemoval = true)
public static double nextDoubleBetween(double minInclu, double maxExclu) {
return rand.nextDouble(minInclu, maxExclu);
}
/**
* Returns a random element from the provided array.
* @param array the array in whith to pick a value randomly.
* @return the value randomly picked from the array, or null if the array is null or empty.
* @param <T> the type of the array elements.
* @see Random#nextInt(int)
*/
public static <T> T arrayElement(T[] array) {
return (array == null || array.length == 0) ? null : array[rand.nextInt(array.length)];
}
/**
* Returns a random element from the provided list.
* @param list the list in whith to pick a value randomly.
* @return the value randomly picked from the list, or null if the array is null or empty.
* @param <T> the type of the list elements.
* @see Random#nextInt(int)
*/
public static <T> T listElement(List<T> list) {
return (list == null || list.isEmpty()) ? null : list.get(rand.nextInt(list.size()));
}
/**
* Returns a random character from the provided string.
* @param str the string in whith to pick a character randomly.
* @return the character randomly picked from the string, or {@code '\0'} if the string is null or empty.
* @see Random#nextInt(int)
*/
public static char stringChar(String str) {
return (str == null || str.isEmpty()) ? '\0' : str.charAt(rand.nextInt(str.length()));
}
/**
* Returns a random value from the provided set.
* @param set the set in which to pick a value randomly.
* @return the value randomly picked from the list, or null if the set is null or empty.
* @param <T> the type of the set elements.
* @implNote The current implementation uses the iterator of the set to pick a random value, since there is no way
* to directly pick a value using an index.
* @throws IllegalStateException if the set has reduced in size during the execution of this method, making the
* iterator reaching the end of the set before getting the value the random generator picked.
*/ */
public static <T> T setElement(Set<T> set) { public static <T> T setElement(Set<T> set) {
if (set.isEmpty()) if (set == null || set.isEmpty())
throw new IllegalArgumentException("set is empty"); return null;
int retI = rand.nextInt(set.size()), i = 0; int retI = rand.nextInt(set.size()), i = 0;
for (T e : set) { for (T e : set) {
if (retI == i) if (retI == i)
return e; return e;
i++; i++;
} }
throw new RuntimeException("Should never go to this line of code"); throw new IllegalStateException("Should never go to this line of code");
} }
/** /**
* Return a value between 0 and the number of parameter minus 1, using the provided frequencies. * Return a value between 0 and the number of parameter minus 1, using the provided frequencies.
* *
* The probability of each value to be returned depends of the frequencies provided. * The probability of each value to be returned depends of the frequencies provided.
* @param f the frequencies of each entries * @param frequencies the frequencies of each entries
* @return the index of an entry, or -1 if it is unable to pick anything (all the frequencies are 0 or there is not provided frequency) * @return the index of an entry, or -1 if it is unable to pick anything (all the frequencies are 0 or there is no provided frequency)
* @throws IllegalArgumentException if frequencies is null.
*/ */
public static int randomIndexOfFrequencies(double... f) { public static int randomIndexOfFrequencies(double... frequencies) {
if (f == null) if (frequencies == null)
throw new IllegalArgumentException("f cannot be null"); throw new IllegalArgumentException("frequencies cannot be null");
int n = f.length; int n = frequencies.length;
double[] fSums = new double[n]; double[] fSums = new double[n];
double sum = 0; double sum = 0;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (f[i] < 0) if (frequencies[i] < 0)
throw new IllegalArgumentException("f[" + i + "] cannot be negative."); throw new IllegalArgumentException("frequencies[" + i + "] cannot be negative.");
fSums[i] = (sum += f[i]); fSums[i] = (sum += frequencies[i]);
} }
double r = rand.nextDouble() * sum; double r = rand.nextDouble() * sum;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
@ -74,20 +125,47 @@ public class RandomUtil {
} }
/**
* A set of characters representing all the lowercase letters of the latin alphabet (only in the ASCII table).
*/
public static final String PASSWORD_CHARSET_LATIN_LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; public static final String PASSWORD_CHARSET_LATIN_LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
/**
* A set of characters representing all the uppercase letters of the latin alphabet (only in the ASCII table).
*/
public static final String PASSWORD_CHARSET_LATIN_UPPERCASE = PASSWORD_CHARSET_LATIN_LOWERCASE.toUpperCase(); public static final String PASSWORD_CHARSET_LATIN_UPPERCASE = PASSWORD_CHARSET_LATIN_LOWERCASE.toUpperCase();
/**
* A set of characters representing all the number digits, from 0 to 9.
*/
public static final String PASSWORD_CHARSET_DIGIT = "0123456789"; public static final String PASSWORD_CHARSET_DIGIT = "0123456789";
/**
* A set of characters representing some visible special characters in the ASCII table.
*/
public static final String PASSWORD_CHARSET_SPECIAL = "@#+*/-;:,.?!='()[]{}&"; public static final String PASSWORD_CHARSET_SPECIAL = "@#+*/-;:,.?!='()[]{}&";
/**
* A set of characters representing uppercase and lowercase latin alphabet letters and digits, exclusing some that
* can be confusing to read (like {@code iIl1} or {@code oO0}).
*/
public static final String PASSWORD_CHARSET_NO_ANBIGUITY = "abcdefghkmnpqrstwxyzACDEFGHKLMNPQRSTWXYZ2345679"; public static final String PASSWORD_CHARSET_NO_ANBIGUITY = "abcdefghkmnpqrstwxyzACDEFGHKLMNPQRSTWXYZ2345679";
/**
* Generate a random password of the provided length, using the characters listed in {@link #PASSWORD_CHARSET_NO_ANBIGUITY}
* @param length the length of the generated password.
* @return the generated password.
*/
public static String randomPassword(int length) { public static String randomPassword(int length) {
return randomPassword(length, PASSWORD_CHARSET_NO_ANBIGUITY); return randomPassword(length, PASSWORD_CHARSET_NO_ANBIGUITY);
} }
/**
* Generate a random password of the provided length, using the provided characters in a string.
* @param length the length of the generated password.
* @param charset the characters to use. Its possible to use of the {@code PASSWORD_*} static strings in this class.
* @return the generated password.
*/
public static String randomPassword(int length, String charset) { public static String randomPassword(int length, String charset) {
char[] pw = new char[length]; char[] pw = new char[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {

View File

@ -3,7 +3,16 @@ package fr.pandacube.lib.util;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
/**
* Provides various methods to manipulate Strings.
*/
public class StringUtil { public class StringUtil {
/**
* Format the provided double, omitting the decimal part if the provided double is strictly equals to a long value.
* @param d the value to convert to string.
* @return a string representation of the double value.
*/
public static String formatDouble(double d) { public static String formatDouble(double d) {
if (d == (long) d) if (d == (long) d)
return String.format("%d", (long) d); return String.format("%d", (long) d);
@ -11,34 +20,62 @@ public class StringUtil {
} }
/** /**
* @param s Chaine de caractère à parcourir * Counts the number of occurence of a speficied character in a string.
* @param c_match le caractère dont on doit retourner le nombre d'occurence * @param string the character sequence to search into.
* @return nombre d'occurence de <b>c_match</b> dans <b>s</b> * @param character the character to count.
* @return the number of occurence of
* @deprecated Because it uses snake_case naming convention. Use {@link #countOccurences(CharSequence, char)} instead.
*/ */
public static int char_count(CharSequence s, char c_match) { @Deprecated(forRemoval = true, since = "2022-07-26")
char[] chars = s.toString().toCharArray(); public static int char_count(CharSequence string, char character) {
return countOccurences(string, character);
}
/**
* Counts the number of occurence of a speficied character in a string.
* @param string the character sequence to search into.
* @param character the character to count.
* @return the number of occurence of
*/
public static int countOccurences(CharSequence string, char character) {
int count = 0; int count = 0;
for (char c : chars) for (char c : string.toString().toCharArray()) {
if (c == c_match) count++; if (c == character) {
count++;
}
}
return count; return count;
} }
/**
* Do like {@link String#join(CharSequence, Iterable)}, but the last separator is different than the others.
public static String joinGrammatically(CharSequence sep1, CharSequence sepFinal, List<String> strings) { * It is usedful when enumerating thins in a sentense, for instance : <code>"a thing<u>, </u>a thing<u> and </u>a thing"</code>
* (the coma being the usual separator, and {@code " and "} being the final separator).
* @param regularSeparator the separator used everywhere except between the two last strings to join.
* @param finalSeparator the separator used between the two last strings to join.
* @param strings the strings to join.
* @return a new string will all the provided {@code strings} joined using the separators.
*/
public static String joinGrammatically(CharSequence regularSeparator, CharSequence finalSeparator, List<String> strings) {
int size = strings == null ? 0 : strings.size(); int size = strings == null ? 0 : strings.size();
return size == 0 ? "" : size == 1 ? strings.get(0) : String.join(sep1, strings.subList(0, --size)) + sepFinal + strings.get(size); return switch (size) {
case 0 -> "";
case 1 -> strings.get(0);
default -> String.join(regularSeparator, strings.subList(0, --size)) + finalSeparator + strings.get(size);
};
} }
/**
* Create a {@link String} that repeats the base character n times.
* @param base the base character.
* @param n the number of repetition.
public static String repeat(char base, int count) { * @return a {@link String} that repeats the base character n times.
char[] chars = new char[count]; */
public static String repeat(char base, int n) {
char[] chars = new char[n];
Arrays.fill(chars, base); Arrays.fill(chars, base);
return new String(chars); return String.valueOf(chars);
} }
} }

View File

@ -6,16 +6,23 @@ import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/**
* Utility class to easily manipulate {@link Throwable}s.
*/
public class ThrowableUtil { public class ThrowableUtil {
/**
* Convert a {@link Throwable} into a {@link String} using the {@link Throwable#printStackTrace(PrintStream)} method,
* so the returned string contains the full stack trace.
* @param t the {@link Throwable}
* @return a {@link String} containing the full stack thace of the provided {@link Throwable}.
*/
public static String stacktraceToString(Throwable t) { public static String stacktraceToString(Throwable t) {
if (t == null) return null; if (t == null) return null;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { try (ByteArrayOutputStream os = new ByteArrayOutputStream();
try (PrintStream ps = new PrintStream(os, false, StandardCharsets.UTF_8)) { PrintStream ps = new PrintStream(os, false, StandardCharsets.UTF_8)) {
t.printStackTrace(ps); t.printStackTrace(ps);
ps.flush(); ps.flush();
}
return os.toString(StandardCharsets.UTF_8); return os.toString(StandardCharsets.UTF_8);
} catch (IOException e) { } catch (IOException e) {
return null; return null;
@ -33,6 +40,7 @@ public class ThrowableUtil {
* @param supp the {@link SupplierException} to run and get the value from. * @param supp the {@link SupplierException} to run and get the value from.
* @return the value returned by the provided supplier. * @return the value returned by the provided supplier.
* @throws RuntimeException if the provided {@link SupplierException} throws a checked exception. * @throws RuntimeException if the provided {@link SupplierException} throws a checked exception.
* @param <T> the type of the returned object
*/ */
public static <T> T wrapEx(SupplierException<T> supp) { public static <T> T wrapEx(SupplierException<T> supp) {
try { try {
@ -58,10 +66,12 @@ public class ThrowableUtil {
/** /**
* Wraps a {@link SupplierException} into a try catch. * Wraps a {@link SupplierException} into a try catch, with special handling of subclasses of
* {@link ReflectiveOperationException}.
* @param supp the {@link SupplierException} to run and get the value from. * @param supp the {@link SupplierException} to run and get the value from.
* @return the value returned by the provided supplier. * @return the value returned by the provided supplier.
* @throws RuntimeException if the provided {@link SupplierException} throws a checked exception. * @throws RuntimeException if the provided {@link SupplierException} throws a checked exception.
* @param <T> the type of the returned object
*/ */
public static <T> T wrapReflectEx(SupplierException<T> supp) { public static <T> T wrapReflectEx(SupplierException<T> supp) {
try { try {
@ -72,7 +82,8 @@ public class ThrowableUtil {
} }
/** /**
* Wraps a {@link RunnableException} into a try catch. * Wraps a {@link RunnableException} into a try catch with special handling of subclasses of
* {@link ReflectiveOperationException}.
* @param run the {@link RunnableException} to run. * @param run the {@link RunnableException} to run.
* @throws RuntimeException if the provided {@link RunnableException} throws a checked exception. * @throws RuntimeException if the provided {@link RunnableException} throws a checked exception.
*/ */
@ -87,20 +98,29 @@ public class ThrowableUtil {
/** /**
* A supplier that can possibly throw a checked exception * A supplier that can possibly throw a checked exception.
*/ */
@FunctionalInterface @FunctionalInterface
public interface SupplierException<T> { public interface SupplierException<T> {
/**
* Gets a result.
* @return a result.
* @throws Exception if implementation failed to run.
*/
T get() throws Exception; T get() throws Exception;
} }
/** /**
* A runnable that can possibly throw a checked exception * A runnable that can possibly throw a checked exception.
*/ */
@FunctionalInterface @FunctionalInterface
public interface RunnableException { public interface RunnableException {
/**
* Run any code implemented.
* @throws Exception if implementation failed to run.
*/
void run() throws Exception; void run() throws Exception;
} }

View File

@ -1,10 +1,13 @@
package fr.pandacube.lib.util; package fr.pandacube.lib.util;
/**
* Provides methods related to Minecraft Java server ticks.
*/
public class Tick { public class Tick {
/** /**
* Returns the number of tick is the provided duration, in second * Returns the number of tick is the provided number of seconds.
* @param seconds the duration in second * @param seconds the duration in second
* @return the same duration as provided, but in Minecraft server ticks * @return the same duration as provided, but in Minecraft server ticks
*/ */
@ -13,7 +16,7 @@ public class Tick {
} }
/** /**
* Returns the number of tick is the provided duration, in second * Returns the number of tick is the provided number of minutes.
* @param minutes the duration in minutes * @param minutes the duration in minutes
* @return the same duration as provided, but in Minecraft server ticks * @return the same duration as provided, but in Minecraft server ticks
*/ */

View File

@ -7,7 +7,6 @@ import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Comparator; import java.util.Comparator;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -17,38 +16,68 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Utility class providing methods to display human readable time and duration, and parse duration strings.
*
* The methods that return date and daytime are hardcoded in French.
*/
public class TimeUtil { public class TimeUtil {
private static final DateTimeFormatter cmpDayOfWeekFormatter = DateTimeFormatter.ofPattern("EEE", Locale.getDefault()); private static final DateTimeFormatter cmpDayOfWeekFormatter = DateTimeFormatter.ofPattern("EEE", Locale.FRENCH);
private static final DateTimeFormatter dayOfWeekFormatter = DateTimeFormatter.ofPattern("EEEE", Locale.getDefault()); private static final DateTimeFormatter dayOfWeekFormatter = DateTimeFormatter.ofPattern("EEEE", Locale.FRENCH);
private static final DateTimeFormatter dayOfMonthFormatter = DateTimeFormatter.ofPattern("d", Locale.getDefault()); private static final DateTimeFormatter dayOfMonthFormatter = DateTimeFormatter.ofPattern("d", Locale.FRENCH);
private static final DateTimeFormatter cmpMonthFormatter = DateTimeFormatter.ofPattern("MMM", Locale.getDefault()); private static final DateTimeFormatter cmpMonthFormatter = DateTimeFormatter.ofPattern("MMM", Locale.FRENCH);
private static final DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MMMM", Locale.getDefault()); private static final DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MMMM", Locale.FRENCH);
private static final DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("uuuu", Locale.getDefault()); private static final DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("uuuu", Locale.FRENCH);
private static final DateTimeFormatter HMSFormatter = DateTimeFormatter.ofPattern("HH:mm:ss", Locale.getDefault()); private static final DateTimeFormatter HMSFormatter = DateTimeFormatter.ofPattern("HH:mm:ss", Locale.FRENCH);
private static final DateTimeFormatter HMFormatter = DateTimeFormatter.ofPattern("HH:mm", Locale.getDefault()); private static final DateTimeFormatter HMFormatter = DateTimeFormatter.ofPattern("HH:mm", Locale.FRENCH);
private static final DateTimeFormatter HFormatter = DateTimeFormatter.ofPattern("H'h'", Locale.getDefault()); private static final DateTimeFormatter HFormatter = DateTimeFormatter.ofPattern("H'h'", Locale.FRENCH);
public static String relativeDateFr(long displayTime, boolean showSeconds, boolean compactWords) { /**
return relativeDateFr(displayTime, * Provides a human readable date of the provided time, with ability to adapt the text relatively to the current
* time (for instance "il y a 13 minutes" (french for "13 minutes ago"))
* <p>
* <b>This method renders the text in French.</b>
*
* @param time the timestamp in milliseconds of the time to diplay.
* @param showSeconds if the returned string should includes seconds (true) or not (false). To have more control
* over the precision, call {@link #relativeDateFr(long, RelativePrecision, DisplayPrecision,
* boolean)}.
* @param compactWords true to use compact words, false to use full words.
* @return a human readable {@link String} representation of the provided time.
*/
public static String relativeDateFr(long time, boolean showSeconds, boolean compactWords) {
return relativeDateFr(time,
showSeconds ? RelativePrecision.SECONDS : RelativePrecision.MINUTES, showSeconds ? RelativePrecision.SECONDS : RelativePrecision.MINUTES,
showSeconds ? DisplayPrecision.SECONDS : DisplayPrecision.MINUTES, showSeconds ? DisplayPrecision.SECONDS : DisplayPrecision.MINUTES,
compactWords); compactWords);
} }
public static String relativeDateFr(long displayTime, RelativePrecision relPrecision, DisplayPrecision dispPrecision, boolean compactWords) { /**
* Provides a human readable date of the provided time, with ability to adapt the text relatively to the current
* time (for instance "il y a 13 minutes" (french for "13 minutes ago"))
* <p>
* <b>This method renders the text in French.</b>
*
* @param time the timestamp in milliseconds of the time to diplay.
* @param relPrecision the precision of the relative text.
* @param dispPrecision the precision of the full date and time.
* @param compactWords true to use compact words, false to use full words.
* @return a human readable {@link String} representation of the provided time.
*/
public static String relativeDateFr(long time, RelativePrecision relPrecision, DisplayPrecision dispPrecision, boolean compactWords) {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
LocalDateTime displayDateTime = toLocalDateTime(displayTime); LocalDateTime displayDateTime = toLocalDateTime(time);
LocalDateTime currentDateTime = toLocalDateTime(currentTime); LocalDateTime currentDateTime = toLocalDateTime(currentTime);
long timeDiff = currentTime - displayTime; long timeDiff = currentTime - time;
long timeDiffSec = timeDiff / 1000; long timeDiffSec = timeDiff / 1000;
if (timeDiffSec < -1) { if (timeDiffSec < -1) {
@ -57,7 +86,7 @@ public class TimeUtil {
if (timeDiffSec > -60) if (timeDiffSec > -60)
return "dans " + (-timeDiffSec) + (compactWords ? "s" : " secondes"); return "dans " + (-timeDiffSec) + (compactWords ? "s" : " secondes");
} }
if (relPrecision.morePreciseOrEqTo(RelativePrecision.MINUTES)) { if (relPrecision.ordinal() >= RelativePrecision.MINUTES.ordinal()) {
if (timeDiffSec > -60) if (timeDiffSec > -60)
return compactWords ? "dans moins d1min" : "dans moins dune minute"; return compactWords ? "dans moins d1min" : "dans moins dune minute";
if (timeDiffSec > -60*2) // dans 2 min if (timeDiffSec > -60*2) // dans 2 min
@ -65,7 +94,7 @@ public class TimeUtil {
if (timeDiffSec > -3600) // dans moins d1h if (timeDiffSec > -3600) // dans moins d1h
return "dans " + (-timeDiffSec/60) + (compactWords ? "min" : " minutes"); return "dans " + (-timeDiffSec/60) + (compactWords ? "min" : " minutes");
} }
if (relPrecision.morePreciseOrEqTo(RelativePrecision.HOURS)) { if (relPrecision.ordinal() >= RelativePrecision.HOURS.ordinal()) {
if (timeDiffSec > -3600) // dans moins d1h if (timeDiffSec > -3600) // dans moins d1h
return compactWords ? "dans moins d1h" : "dans moins dune heure"; return compactWords ? "dans moins d1h" : "dans moins dune heure";
if (timeDiffSec > -3600*2) // dans moins de 2h if (timeDiffSec > -3600*2) // dans moins de 2h
@ -73,16 +102,16 @@ public class TimeUtil {
if (timeDiffSec > -3600*12) // dans moins de 12h if (timeDiffSec > -3600*12) // dans moins de 12h
return "dans " + (-timeDiffSec/3600) + (compactWords ? "h" : " heures"); return "dans " + (-timeDiffSec/3600) + (compactWords ? "h" : " heures");
} }
if (relPrecision.morePreciseOrEqTo(RelativePrecision.DAYS)) { if (relPrecision.ordinal() >= RelativePrecision.DAYS.ordinal()) {
LocalDateTime nextMidnight = LocalDateTime.of(currentDateTime.getYear(), currentDateTime.getMonth(), currentDateTime.getDayOfMonth(), 0, 0).plusDays(1); LocalDateTime nextMidnight = LocalDateTime.of(currentDateTime.getYear(), currentDateTime.getMonth(), currentDateTime.getDayOfMonth(), 0, 0).plusDays(1);
if (displayDateTime.isBefore(nextMidnight)) // aujourd'hui if (displayDateTime.isBefore(nextMidnight)) // aujourd'hui
return "aujourdhui à " + dayTimeFr(displayTime, dispPrecision); return "aujourdhui à " + dayTimeFr(time, dispPrecision);
if (displayDateTime.isBefore(nextMidnight.plusDays(1))) // demain if (displayDateTime.isBefore(nextMidnight.plusDays(1))) // demain
return "demain à " + dayTimeFr(displayTime, dispPrecision); return "demain à " + dayTimeFr(time, dispPrecision);
if (displayDateTime.isBefore(nextMidnight.plusDays(5))) // dans moins d'1 semaine if (displayDateTime.isBefore(nextMidnight.plusDays(5))) // dans moins d'1 semaine
return (compactWords ? cmpDayOfWeekFormatter : dayOfWeekFormatter).format(displayDateTime) + " " return (compactWords ? cmpDayOfWeekFormatter : dayOfWeekFormatter).format(displayDateTime) + " "
+ dayOfMonthFormatter.format(displayDateTime) + " à " + dayOfMonthFormatter.format(displayDateTime) + " à "
+ dayTimeFr(displayTime, dispPrecision); + dayTimeFr(time, dispPrecision);
} }
} }
@ -95,7 +124,7 @@ public class TimeUtil {
if (timeDiffSec < 60) // ya moins d'1 min if (timeDiffSec < 60) // ya moins d'1 min
return "il y a " + timeDiffSec + (compactWords ? "s" : " secondes"); return "il y a " + timeDiffSec + (compactWords ? "s" : " secondes");
} }
if (relPrecision.morePreciseOrEqTo(RelativePrecision.MINUTES)) { if (relPrecision.ordinal() >= RelativePrecision.MINUTES.ordinal()) {
if (timeDiffSec < 60) // ya moins d'1 min if (timeDiffSec < 60) // ya moins d'1 min
return compactWords ? "il y a moins d1min" : "il y a moins dune minute"; return compactWords ? "il y a moins d1min" : "il y a moins dune minute";
if (timeDiffSec < 60*2) // ya moins de 2 min if (timeDiffSec < 60*2) // ya moins de 2 min
@ -103,7 +132,7 @@ public class TimeUtil {
if (timeDiffSec < 3600) // ya moins d'1h if (timeDiffSec < 3600) // ya moins d'1h
return "il y a " + (timeDiffSec/60) + (compactWords ? "min" : " minutes"); return "il y a " + (timeDiffSec/60) + (compactWords ? "min" : " minutes");
} }
if (relPrecision.morePreciseOrEqTo(RelativePrecision.HOURS)) { if (relPrecision.ordinal() >= RelativePrecision.HOURS.ordinal()) {
if (timeDiffSec < 3600) // ya moins d'1h if (timeDiffSec < 3600) // ya moins d'1h
return "il y a moins dune heure"; return "il y a moins dune heure";
if (timeDiffSec < 3600*2) // ya moins de 2h if (timeDiffSec < 3600*2) // ya moins de 2h
@ -111,60 +140,126 @@ public class TimeUtil {
if (timeDiffSec < 3600*12) // ya moins de 12h if (timeDiffSec < 3600*12) // ya moins de 12h
return "il y a " + (timeDiffSec/3600) + " heures"; return "il y a " + (timeDiffSec/3600) + " heures";
} }
if (relPrecision.morePreciseOrEqTo(RelativePrecision.DAYS)) { if (relPrecision.ordinal() >= RelativePrecision.DAYS.ordinal()) {
LocalDateTime lastMidnight = LocalDateTime.of(currentDateTime.getYear(), currentDateTime.getMonth(), currentDateTime.getDayOfMonth(), 0, 0); LocalDateTime lastMidnight = LocalDateTime.of(currentDateTime.getYear(), currentDateTime.getMonth(), currentDateTime.getDayOfMonth(), 0, 0);
if (!displayDateTime.isBefore(lastMidnight)) // aujourd'hui if (!displayDateTime.isBefore(lastMidnight)) // aujourd'hui
return "aujourdhui à " + dayTimeFr(displayTime, dispPrecision); return "aujourdhui à " + dayTimeFr(time, dispPrecision);
if (!displayDateTime.isBefore(lastMidnight.minusDays(1))) // hier if (!displayDateTime.isBefore(lastMidnight.minusDays(1))) // hier
return "hier à " + dayTimeFr(displayTime, dispPrecision); return "hier à " + dayTimeFr(time, dispPrecision);
if (!displayDateTime.isBefore(lastMidnight.minusDays(6))) // ya moins d'1 semaine if (!displayDateTime.isBefore(lastMidnight.minusDays(6))) // ya moins d'1 semaine
return (compactWords ? cmpDayOfWeekFormatter : dayOfWeekFormatter).format(displayDateTime) + " dernier à " return (compactWords ? cmpDayOfWeekFormatter : dayOfWeekFormatter).format(displayDateTime) + " dernier à "
+ dayTimeFr(displayTime, dispPrecision); + dayTimeFr(time, dispPrecision);
} }
} }
return fullDateFr(displayTime, dispPrecision, true, compactWords); return fullDateFr(time, dispPrecision, true, compactWords);
} }
/**
* Enumaration of different level of precision to display a relative time.
*/
public enum RelativePrecision { public enum RelativePrecision {
NONE, DAYS, HOURS, MINUTES, SECONDS; /**
* No relative display.
public boolean morePreciseThan(RelativePrecision o) { return ordinal() > o.ordinal(); } */
public boolean lessPreciseThan(RelativePrecision o) { return ordinal() < o.ordinal(); } NONE,
public boolean morePreciseOrEqTo(RelativePrecision o) { return ordinal() >= o.ordinal(); } /**
public boolean lessPreciseOrEqTo(RelativePrecision o) { return ordinal() <= o.ordinal(); } * Days precision for relative display.
*/
DAYS,
/**
* Hours precision for relative display.
*/
HOURS,
/**
* Minutes precision for relative display.
*/
MINUTES,
/**
* Seconds precision for relative display.
*/
SECONDS;
} }
/**
* Enumaration of different level of precision to display a date and daytime.
*/
public enum DisplayPrecision { public enum DisplayPrecision {
DAYS, HOURS, MINUTES, SECONDS /**
* Display only the date.
*/
DAYS,
/**
* Display the date and the hour of the day.
*/
HOURS,
/**
* Display the date and the time of the day up to the minute.
*/
MINUTES,
/**
* Display the date and the time of the day up to the second.
*/
SECONDS
} }
public static String fullDateFr(long displayTime, boolean showSeconds, boolean showWeekday, boolean compactWords) { /**
return fullDateFr(displayTime, showSeconds ? DisplayPrecision.SECONDS : DisplayPrecision.MINUTES, showWeekday, compactWords); * Returns a string representation of the date (and eventually day time) of the provided timestamp.
* <p>
* <b>This method renders the text in French.</b>
*
* @param timestamp the time to represent in the returned string.
* @param showSeconds if the returned string should includes seconds (true) or not (false). To have more control
* over the precision, call {@link #fullDateFr(long, DisplayPrecision, boolean, boolean)}.
* @param showWeekday true to show the week day, false otherwise.
* @param compactWords true to use compact words, false to use full words.
* @return a string representation of the date (and eventually day time) of the provided timestamp.
*/
public static String fullDateFr(long timestamp, boolean showSeconds, boolean showWeekday, boolean compactWords) {
return fullDateFr(timestamp, showSeconds ? DisplayPrecision.SECONDS : DisplayPrecision.MINUTES, showWeekday, compactWords);
} }
public static String fullDateFr(long displayTime, DisplayPrecision precision, boolean showWeekday, boolean compactWords) { /**
LocalDateTime displayDateTime = toLocalDateTime(displayTime); * Returns a string representation of the date (and eventually day time) of the provided timestamp.
* <p>
* <b>This method renders the text in French.</b>
*
* @param timestamp the time to represent in the returned string.
* @param precision the {@link DisplayPrecision} fo the returned string.
* @param showWeekday true to show the week day, false otherwise.
* @param compactWords true to use compact words, false to use full words.
* @return a string representation of the date (and eventually day time) of the provided timestamp.
*/
public static String fullDateFr(long timestamp, DisplayPrecision precision, boolean showWeekday, boolean compactWords) {
LocalDateTime displayDateTime = toLocalDateTime(timestamp);
String ret = (showWeekday ? ((compactWords ? cmpDayOfWeekFormatter : dayOfWeekFormatter).format(displayDateTime) + " ") : "") String ret = (showWeekday ? ((compactWords ? cmpDayOfWeekFormatter : dayOfWeekFormatter).format(displayDateTime) + " ") : "")
+ dayOfMonthFormatter.format(displayDateTime) + " " + dayOfMonthFormatter.format(displayDateTime) + " "
+ (compactWords ? cmpMonthFormatter : monthFormatter).format(displayDateTime) + " " + (compactWords ? cmpMonthFormatter : monthFormatter).format(displayDateTime) + " "
+ yearFormatter.format(displayDateTime); + yearFormatter.format(displayDateTime);
if (precision == DisplayPrecision.DAYS) if (precision == DisplayPrecision.DAYS)
return ret; return ret;
return ret + " à " + dayTimeFr(displayTime, precision); return ret + " à " + dayTimeFr(timestamp, precision);
} }
public static String dayTimeFr(long displayTime, DisplayPrecision precision) { /**
* Returns a string representation of the time of the day of the provided timestamp.
* <p>
* <b>This method renders the text in French.</b>
*
* @param timestamp the time to represent in the returned string.
* @param precision the {@link DisplayPrecision} fo the returned string.
* @return a string representation of the time of the day of the provided timestamp.
*/
public static String dayTimeFr(long timestamp, DisplayPrecision precision) {
DateTimeFormatter tFormatter = switch(precision) { DateTimeFormatter tFormatter = switch(precision) {
case HOURS -> HFormatter; case HOURS -> HFormatter;
case MINUTES -> HMFormatter; case MINUTES -> HMFormatter;
case SECONDS -> HMSFormatter; case SECONDS -> HMSFormatter;
default -> throw new IllegalArgumentException("precision"); default -> throw new IllegalArgumentException("precision");
}; };
return tFormatter.format(toLocalDateTime(displayTime)); return tFormatter.format(toLocalDateTime(timestamp));
} }
@ -173,14 +268,16 @@ public class TimeUtil {
} }
/**
* Converts the provided duration into a human readable {@link String}.
* @param msDuration the duration in millisecond.
* @param hUnit the biggest unit of time to display.
* @param lUnit the smallest unit of time to display.
* @param spaces true to put spaces between time units (e.g.: {@code "1s 500ms"}) or false otherwise (e.g.: {@code "1s500ms"})).
* @param fr true to use French unit symbols (it only changes the day symbol from "d" to "j").
* @param leadingZeros to use leading zeros when necessary in front of some durations.
* @return a {@link String} representation of the duration.
*/
public static String durationToLongString(long msDuration, TimeUnit hUnit, TimeUnit lUnit, boolean spaces, boolean fr, boolean leadingZeros) { public static String durationToLongString(long msDuration, TimeUnit hUnit, TimeUnit lUnit, boolean spaces, boolean fr, boolean leadingZeros) {
if (lUnit.compareTo(hUnit) > 0) { if (lUnit.compareTo(hUnit) > 0) {
TimeUnit tmp = lUnit; TimeUnit tmp = lUnit;
@ -210,14 +307,19 @@ public class TimeUtil {
.map(u -> { .map(u -> {
long v = u.convert(remainingTime.get(), TimeUnit.MILLISECONDS); long v = u.convert(remainingTime.get(), TimeUnit.MILLISECONDS);
remainingTime.addAndGet(TimeUnit.MILLISECONDS.convert(-v, u)); remainingTime.addAndGet(TimeUnit.MILLISECONDS.convert(-v, u));
return toString(v, leadingZeros ? timeUnitToLeftPadLength(u) : 1) + timeUnitToSuffix(u, fr); return toStringWithPaddingZeros(v, leadingZeros ? timeUnitToLeftPadLength(u) : 1) + timeUnitToSuffix(u, fr);
}) })
.collect(Collectors.joining(spaces ? " " : "")); .collect(Collectors.joining(spaces ? " " : ""));
// ensure there is at least something to display (for instance : "0s") // ensure there is at least something to display (for instance : "0s")
return oneDisplayed.get() ? ret : (toString(0, leadingZeros ? timeUnitToLeftPadLength(lUnit) : 1) + timeUnitToSuffix(lUnit, fr)); return oneDisplayed.get() ? ret : (toStringWithPaddingZeros(0, leadingZeros ? timeUnitToLeftPadLength(lUnit) : 1) + timeUnitToSuffix(lUnit, fr));
} }
/**
* Provides a unit symbol for the provided {@link TimeUnit}.
* @param u the {@link TimeUnit}.
* @param fr true to use French unit symbols (it only changes the {@link TimeUnit#DAYS} symbol from "d" to "j").
* @return a unit symbol for the provided {@link TimeUnit}.
*/
public static String timeUnitToSuffix(TimeUnit u, boolean fr) { public static String timeUnitToSuffix(TimeUnit u, boolean fr) {
return switch (u) { return switch (u) {
case DAYS -> fr ? "j" : "d"; case DAYS -> fr ? "j" : "d";
@ -230,6 +332,12 @@ public class TimeUtil {
}; };
} }
/**
* Indicate the 0-padded length of a number for the provided {@link TimeUnit}.
* Will returns 3 for below-second time units, 2 for seconds, munutes and hours and 1 otherwise.
* @param u the {@link TimeUnit}
* @return the 0-padded length of a number for the provided {@link TimeUnit}.
*/
public static int timeUnitToLeftPadLength(TimeUnit u) { public static int timeUnitToLeftPadLength(TimeUnit u) {
return switch (u) { return switch (u) {
case NANOSECONDS, MICROSECONDS, MILLISECONDS -> 3; case NANOSECONDS, MICROSECONDS, MILLISECONDS -> 3;
@ -238,7 +346,14 @@ public class TimeUtil {
}; };
} }
public static String toString(long value, int leftPad) { /**
* Converts the provided long to a {@link String} and eventually prepend any {@code "0"} necessary to make the
* returned strings length at least {@code leftPad}.
* @param value the value to convert to {@link String}.
* @param leftPad the minimal length of the returned String.
* @return the string representation of the provided value, with eventual zeros prepended.
*/
public static String toStringWithPaddingZeros(long value, int leftPad) {
String valueStr = Long.toString(value); String valueStr = Long.toString(value);
int padding = leftPad - valueStr.length(); int padding = leftPad - valueStr.length();
if (padding <= 0) if (padding <= 0)
@ -255,6 +370,7 @@ public class TimeUtil {
* Equivalent to {@link #durationToLongString(long, TimeUnit, TimeUnit, boolean, boolean, boolean) TimeUnit.durationToLongString(msDuration, TimeUnit.DAYS, milliseconds ? TimeUnit.MILLISECONDS : TimeUnit.SECONDS, true, true, false)} * Equivalent to {@link #durationToLongString(long, TimeUnit, TimeUnit, boolean, boolean, boolean) TimeUnit.durationToLongString(msDuration, TimeUnit.DAYS, milliseconds ? TimeUnit.MILLISECONDS : TimeUnit.SECONDS, true, true, false)}
* @param msDuration the duration in ms * @param msDuration the duration in ms
* @param milliseconds if the milliseconds are displayed or not * @param milliseconds if the milliseconds are displayed or not
* @return a {@link String} representation of the duration.
*/ */
public static String durationToString(long msDuration, boolean milliseconds) { public static String durationToString(long msDuration, boolean milliseconds) {
return durationToLongString(msDuration, TimeUnit.DAYS, milliseconds ? TimeUnit.MILLISECONDS : TimeUnit.SECONDS, true, true, false); return durationToLongString(msDuration, TimeUnit.DAYS, milliseconds ? TimeUnit.MILLISECONDS : TimeUnit.SECONDS, true, true, false);
@ -263,6 +379,7 @@ public class TimeUtil {
/** /**
* Equivalent to {@link #durationToLongString(long, TimeUnit, TimeUnit, boolean, boolean, boolean) TimeUnit.durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, true, true, false)} * Equivalent to {@link #durationToLongString(long, TimeUnit, TimeUnit, boolean, boolean, boolean) TimeUnit.durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, true, true, false)}
* @param msDuration the duration in ms * @param msDuration the duration in ms
* @return a {@link String} representation of the duration.
*/ */
public static String durationToString(long msDuration) { public static String durationToString(long msDuration) {
return durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, true, true, false); return durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, true, true, false);
@ -271,6 +388,7 @@ public class TimeUtil {
/** /**
* Equivalent to {@link #durationToLongString(long, TimeUnit, TimeUnit, boolean, boolean, boolean) TimeUnit.durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, false, false, false)} * Equivalent to {@link #durationToLongString(long, TimeUnit, TimeUnit, boolean, boolean, boolean) TimeUnit.durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, false, false, false)}
* @param msDuration the duration in ms * @param msDuration the duration in ms
* @return a {@link String} representation of the duration.
*/ */
public static String durationToParsableString(long msDuration) { public static String durationToParsableString(long msDuration) {
return durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, false, false, false); return durationToLongString(msDuration, TimeUnit.DAYS, TimeUnit.SECONDS, false, false, false);
@ -279,9 +397,14 @@ public class TimeUtil {
/** /**
* @see <a href="https://github.com/EssentialsX/Essentials/blob/2.x/Essentials/src/main/java/com/earth2me/essentials/utils/DateUtil.java">Essentials DateUtil#parseDuration(String, boolean)</a> * Parse a duration string into a time in the past of future, relative to now.
* Source: <a href="https://github.com/EssentialsX/Essentials/blob/2.x/Essentials/src/main/java/com/earth2me/essentials/utils/DateUtil.java">Essentials DateUtil#parseDuration(String, boolean)</a>
* @param time the duration to parse.
* @param future thur to return the time in the future, false for the time in the past.
* @return the computed timestamp in millisecond.
* @throws IllegalArgumentException if the format is not valid.
*/ */
public static long parseDuration(String time, boolean future) throws Exception { public static long parseDuration(String time, boolean future) {
@SuppressWarnings("RegExpSimplifiable") @SuppressWarnings("RegExpSimplifiable")
Pattern timePattern = Pattern.compile("(?:([0-9]+)\\s*y[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*mo[a-z]*[,\\s]*)?" Pattern timePattern = Pattern.compile("(?:([0-9]+)\\s*y[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*mo[a-z]*[,\\s]*)?"
+ "(?:([0-9]+)\\s*w[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*d[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*w[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*d[a-z]*[,\\s]*)?"
@ -297,12 +420,14 @@ public class TimeUtil {
int seconds = 0; int seconds = 0;
boolean found = false; boolean found = false;
while (m.find()) { while (m.find()) {
if (m.group() == null || m.group().isEmpty()) continue; if (m.group() == null || m.group().isEmpty())
for (int i = 0; i < m.groupCount(); i++) continue;
for (int i = 0; i < m.groupCount(); i++) {
if (m.group(i) != null && !m.group(i).isEmpty()) { if (m.group(i) != null && !m.group(i).isEmpty()) {
found = true; found = true;
break; break;
} }
}
if (found) { if (found) {
if (m.group(1) != null && !m.group(1).isEmpty()) years = Integer.parseInt(m.group(1)); if (m.group(1) != null && !m.group(1).isEmpty()) years = Integer.parseInt(m.group(1));
if (m.group(2) != null && !m.group(2).isEmpty()) months = Integer.parseInt(m.group(2)); if (m.group(2) != null && !m.group(2).isEmpty()) months = Integer.parseInt(m.group(2));
@ -314,7 +439,8 @@ public class TimeUtil {
break; break;
} }
} }
if (!found) throw new Exception("Format de durée invalide"); if (!found)
throw new IllegalArgumentException("Invalid duration format");
Calendar c = new GregorianCalendar(); Calendar c = new GregorianCalendar();
if (years > 0) c.add(Calendar.YEAR, years * (future ? 1 : -1)); if (years > 0) c.add(Calendar.YEAR, years * (future ? 1 : -1));
if (months > 0) c.add(Calendar.MONTH, months * (future ? 1 : -1)); if (months > 0) c.add(Calendar.MONTH, months * (future ? 1 : -1));
@ -325,15 +451,8 @@ public class TimeUtil {
if (seconds > 0) c.add(Calendar.SECOND, seconds * (future ? 1 : -1)); if (seconds > 0) c.add(Calendar.SECOND, seconds * (future ? 1 : -1));
Calendar max = new GregorianCalendar(); Calendar max = new GregorianCalendar();
max.add(Calendar.YEAR, 10); max.add(Calendar.YEAR, 10);
if (c.after(max)) return max.getTimeInMillis(); return c.after(max) ? max.getTimeInMillis() : c.getTimeInMillis();
return c.getTimeInMillis();
} }
public static final List<String> DURATION_SUFFIXES = List.of("y", "mo", "w", "d", "h", "m", "s");
} }