Drools - How to apply advanced filtering using a window? - filtering

Good day everyone,
Currently I'm working with a simple temperature sensor that basically updates 4 times a second on the current temperature, and filter using the following rule in Drools:
rule "temperature detected"
when
$acc: Number ( doubleValue > 22.0 ) from accumulate(
$sensorMessage: SensorMessage($topic: topic, $timeStamp: timestamp, $data: data) over window:time ( 10s ) from entry-point "temperature_sensor",
average ( $sensorMessage.getDouble("temperature", -100) )
)
then
logger.info("Received temperature data > 22.0 -> " + $acc);
end
This, however, logs the console after EVERY sensor update as long as the accumulated temperature average is larger than 22, over a window of 10 seconds.
This of course, is far from ideal.
Would it be possible to, after a sensor update has been received, continue listening UNTIL no more updates are received for, say, 3 seconds. Then log the starting time of when a sensor update first is detected, and the ending time. And only log these two timestamps if at least 10 updates have been received altogether, and some criterium is met.
Example scenarios (T being some target temperature):
If a motion sensor sends 20 updates within 2 seconds, the time of the first update and time of the last update is logged.
If a motion sensor sends 6 updates in 1 second, then another 6 updates after 5 seconds, nothing happens, as we expect more motion sensor updates to properly classify it as motion.
If a temperature sensor sends 10 updates within 1 second, and the average of all 10 pings is <= T, nothing happens, however, if it does exceed T, we log a single temperature "alert".

This is what I came-up with after few hours. Some requirements are not clear for me, like what is 'the last update is logged', each one is 'the last'. Following may still require a work to do, but you may get general idea: we have single object in the session we update and monitor.
import java.lang.Double;
import java.util.ArrayList;
declare SensorMessage
#role(event)
#timestamp(timestamp)
#expires(10s)
end
rule "temperature monitoring"
when
$meta: SensorMetadata()
$acc: Double () from accumulate(
$message: SensorMessage() over window:time(1s),
average ($message.getTemperature())
)
then
$meta.setTemperature($acc);
update($meta);
end
rule "temperature activated"
when
$meta: SensorMetadata(highTemperature == false, temperature >= $meta.targetTemperature)
$lastMessage: SensorMessage($lastTimestamp: timestamp)
not (SensorMessage(timestamp > $lastMessage.timestamp))
then
$meta.setLastActivated($lastMessage.getTimestamp());
$meta.setHighTemperature(true);
update($meta);
System.out.printf("temperature activated %.2f%n", $meta.getTemperature());
end
rule "temperature deactivated"
when
$meta: SensorMetadata(highTemperature == true, temperature < $meta.targetTemperature)
$lastMessage: SensorMessage($lastTimestamp: timestamp)
not (SensorMessage(timestamp > $lastMessage.timestamp))
then
$meta.setHighTemperature(false);
update($meta);
System.out.printf("temperature deactivated %.2f%n", $meta.getTemperature());
end
rule "throttle state activated"
when
$meta: SensorMetadata(throttleState == false)
$lastMessage: SensorMessage($lastTimestamp: timestamp)
not (SensorMessage(timestamp > $lastMessage.timestamp))
$messages: ArrayList(size > 20) from collect(
$message: SensorMessage() over window:time(1s)
)
then
$meta.setLastThrottled($lastMessage.getTimestamp());
$meta.setThrottleState(true);
update($meta);
System.out.printf("throttle state activated %d%n", $messages.size());
end
rule "throttle state deactivated"
when
$meta: SensorMetadata(throttleState == true)
$lastMessage: SensorMessage($lastTimestamp: timestamp)
not (SensorMessage(timestamp > $lastMessage.timestamp))
$messages: ArrayList(size <= 20) from collect(
$message: SensorMessage() over window:time(1s)
)
then
$meta.setThrottleState(false);
update($meta);
System.out.printf("throttle state deactivated %d%n", $messages.size());
end
the test
#DroolsSession(resources = "file:src/main/resources/draft/rule.drl", showStateTransitionPopup = true, ignoreRules = "* monitoring")
public class PlaygroundTest extends DroolsAssert {
#RegisterExtension
public DroolsAssert droolsAssert = this;
#Test
#TestRules(ignore = "throttle *", expectedCount = {
"2", "temperature activated",
"2", "temperature deactivated" })
public void testTemperatureActivationDeactivation() throws IOException {
insert(new SensorMetadata(22));
Date date = new Date(0);
for (int i = 0; i < 100; i++) {
double temperature = i == 80 ? 100 : i == 81 ? -100 : i < 50 ? i : 1 / i;
insertAndFire(new SensorMessage(date, temperature));
date = addMilliseconds(date, 1);
advanceTime(MILLISECONDS, 1);
}
advanceTime(10, SECONDS);
assertFactsCount(1);
}
#Test
#TestRules(ignore = "temperature *", expectedCount = {
"2", "throttle state activated",
"2", "throttle state deactivated" })
public void testThrottleMode() throws IOException {
insert(new SensorMetadata(22));
Date date = new Date(0);
for (int i = 0; i < 100; i++) {
insertAndFire(new SensorMessage(date, 22));
int advanceTime = i * 3;
date = addMilliseconds(date, advanceTime);
advanceTime(MILLISECONDS, advanceTime);
}
advanceTime(10, SECONDS);
assertFactsCount(1);
}
}
and models
public class SensorMessage {
private Date timestamp;
private double temperature;
public SensorMessage(Date timestamp, double temperature) {
this.timestamp = timestamp;
this.temperature = temperature;
}
// getters
}
public class SensorMetadata {
private volatile Date lastActivated;
private volatile Date lastThrottled;
private double temperature;
private boolean highTemperature;
private boolean throttleState;
private double targetTemperature;
public SensorMetadata(double targetTemperature) {
this.targetTemperature = targetTemperature;
}
// getters and setters
}

Related

How to include triggered record to my logic?

I have 2 custom objects: Game__c and Bet__c. I need to create trigger in which the system should calculate Game Rating based on the Total Money Bet (more money means higher rating).
TotalMoneyBet__c - its Roll-up Summay filed on Game__c
Every time a new bet is created, recalculate and set the rating: max totalmoneybet = gamerating 1
I've write a code and its working but not include triggered bet. Can you help me with this issue?
public class CalculateGameRatingTriggerHandler {
public static void afterInsert(List<Bet__c> triggeredBet) {
List<Game__c> topGames = [SELECT Id, TotalMoneyBet__c, GameRating__c
FROM Game__c
ORDER BY TotalMoneyBet__c DESC LIMIT 10];
for(Integer i = 0; i < 10; i++) {
for(Game__c game : topGames) {
topGames[i].GameRating__c = i+1;
}
}
update topGames;
}
}
trigger BetTriggerSecond on Bet__c (after insert) {
CalculateGameRatingTriggerHandler.afterInsert(trigger.new);
}

Drools Fusion SessionPseudoClock Not working as expected

I am trying to set KieSession date to same date with the date variable in my object before running the rules. I use this configuration to create my KieSession
KieSessionConfiguration configuration = KieServices.Factory.get().newKieSessionConfiguration();
configuration.setOption(ClockTypeOption.get("pseudo"));
Before running the rules I use advanceTime() to set the date of the session to desired date.
final SessionPseudoClock clock = kSession.getSessionClock();
clock.advanceTime(object.getDate().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
final List<Command<?>> commands = new ArrayList<>();
commands.add(CommandFactory.newInsertElements(objects)
commands.add(CommandFactory.newInsert(object, "object"));
final ExecutionResults results = kSession.execute(CommandFactory.newBatchExecution(commands));
This resulted misfires in the rules that uses sliding windows. Lets say checking the objects that passed in 1 hour and I don't have any in the last hour. I only have 3 objects day before. Here is an example dataset of objects.
objects: [
{
clientId: "id",
date: 2021-02-09T12:00:38.249Z,
...
}
{
clientId: "id",
date: 2021-02-09T13:00:38.249Z,
...
}
{
clientId: "id",
date: 2021-02-09T14:00:38.249Z,
...
}
]
I have a rule which checks if there are more than 2 objects with the same clientId over 1 hour.
$object : Object($clientId : clientId)
List( size > 2 ) from collect ( Object( this before $object, clientId == $clientId ) over window:time(1h))
When I pass an object with these values. The rule above returns true but we clearly don't have any objects that has a date within last hour.
{ clientId: "id", date: 2021-02-10T14:00:38.249Z, ... }
I believe this is broken because of the new configuration as it was working previously (when I did not try to change session clock) but I want session date to be equal to object date. Anyone has ideas what is the problem here and how to fix it?
As Roddy of the Frozen Peas made the remark, you need to manipulate SessionPseudoClock in between every insert of your events. I'd implement a new Command, extend InsertElementsCommand and #Override it's execute method:
#Override
public Collection<FactHandle> execute(Context context) {
KieSession kSession = ((RegistryContext) context).lookup(KieSession.class);
List<FactHandle> handles = new ArrayList<>();
Object workingMemory = StringUtils.isEmpty(super.getEntryPoint()) ?
kSession :
kSession.getEntryPoint(super.getEntryPoint());
SessionPseudoClock clock = kSession.getSessionClock();
super.objects.forEach(event -> {
clock.advanceTime(((YourObject) event).getDate().getTime() - clock.getCurrentTime(), TimeUnit.MILLISECONDS);
handles.add(((EntryPoint) workingMemory).insert(event));
});
...
return handles;
}
and instead of:
commands.add(CommandFactory.newInsertElements(objects));
I'd:
commands.add(new YourInsertElementsCommand(objects));

How to make a live counter in unity3d

I recently started using Unity3D and made a few levels. The only problem I have right now is how can I get a live counter?
So my character dies when he hits and certain object.
I want my character to get 3 lives maximum, and get -1 live when he hits that object.
And that it keeps the data when he dies so you wouldn't get lives back if you restart the app.
And after a certain amount of minutes he gets +1 live.
Thank you :)
While your game running. just create a variable counterTime to count time, whenever counterTime pass certain amount of time you want reset counterTime to 0 and increase your life.
When user quit your app, save last time to PlayerPref, eg:
PlayerPref.SaveString("LastTime", DateTime.Now);
When user comback game, just check duration between last time and now to calculate total life need added. eg:
DateTime lastTime = DateTime.Parse(PlayerPref.GetString("LastTime"));
TimeSpan timeDif= DateTime.Now - lastTime;
int duration = timeDif.TotalSeconds;
You can use PlayerPrefs.SetInt , PlayerPrefs.GetInt for storing and reading your player's hp in file storage. Read more about it here:
https://docs.unity3d.com/ScriptReference/PlayerPrefs.html
As for Giving player +1 hp after a few minutes you can store DateTime.Now in a PlayerPrefs variable whenever you give your player some hp and use TimeSpan and TotalMinutesPassed:
TimeSpan passedTime = DateTime.Now - lastStoredDateTime;
int totalMinutesPassed = passedTime.TotalMinutes;
Should go sth like this i guess(didnt test this code just showing a general idea) :
void SetPlayerLives(int lives)
{
playerLives = lives;
PlayerPrefs.SetInt("player-lives",playerLives);
}
//TODO: also sth like => int GetPlayerLives() function
void CheckLiveRegen() //call this function whenever you want to check live regen:
{
int LIVE_REGEN_MINUTES = 5; //regen 1 live every 5 minutes
DateTime lastStoredDateTime = DateTime.Parse(PlayerPrefs.GetString("last-live-regen", DateTime.Now.ToString()));
TimeSpan passedTime = DateTime.Now - lastStoredDateTime;
double totalMinutesPassed = passedTime.TotalMinutes;
if(totalMinutesPassed >= LIVE_REGEN_MINUTES)
{
int val = (int) totalMinutesPassed / LIVE_REGEN_MINUTES;
// Add val to your player lives! + store new lives value
SetPlayerLives(playerLives+val);
//update last-live-regen value:
PlayerPrefs.SetString("last-live-regen", DateTime.Now.ToString());
}
}
Note: DateTime , TimeSpan classes have some bugs (specially in android platform) in versions older than 2017.4 (LTS) Make sure you log values and check if functions are working properly.
https://forum.unity.com/threads/android-datetime-now-is-wrong.488380/
check out the following link to understand how to create a life counter in unity
http://codesaying.com/life-counter-in-unity/
In order to calculate the time that was lapsed since you last shut down the game, you should save the last time playerprefs in the function OnApplicationPause and calcuate the timelapsed in the Awake Function.
void Awake () {
if(!PlayerPrefs.HasKey("Lives")){
PlayerPrefs.SetString("LifeUpdateTime", DateTime.Now.ToString());
}
lives = PlayerPrefs.GetInt("Lives", maxLives);
//update life counter only if lives are less than maxLives
if (lives < maxLives)
{
float timerToAdd = (float)(System.DateTime.Now - Convert.ToDateTime(PlayerPrefs.GetString("LifeUpdateTime"))).TotalSeconds;
UpdateLives(timerToAdd);
}
}
void UpdateLives(double timerToAdd ){
if (lives < maxLives)
{
int livesToAdd = Mathf.FloorToInt((float)timerToAdd / lifeReplenishTime);
timerForLife = (float)timerToAdd % lifeReplenishTime;
lives += livesToAdd;
if (lives > maxLives)
{
lives = maxLives;
timerForLife = 0;
}
PlayerPrefs.SetString("LifeUpdateTime", DateTime.Now.AddSeconds(-timerForLife).ToString());
}else{
PlayerPrefs.SetString("LifeUpdateTime", DateTime.Now.ToString());
}
}
void OnApplicationPause(bool isPause)
{
if (isPause)
{
timeOfPause = System.DateTime.Now;
}
else
{
if(timeOfPause == default(DateTime)){
timeOfPause = System.DateTime.Now;
}
float timerToAdd = (float)(System.DateTime.Now - timeOfPause).TotalSeconds;
timerForLife += timerToAdd;
UpdateLives(timerForLife);
}
}
}

JasperSoft Studio - How is the best way to to print a period of time (minutes) as hh:mm? [duplicate]

I need to convert minutes to hours and minutes in java. For example 260 minutes should be 4:20. can anyone help me how to do convert it.
If your time is in a variable called t
int hours = t / 60; //since both are ints, you get an int
int minutes = t % 60;
System.out.printf("%d:%02d", hours, minutes);
It couldn't get easier
Addendum from 2021:
Please notice that this answer is about the literal meaning of the question: how to convert an amount of minute to hours + minutes. It has nothing to do with time, time zones, AM/PM...
If you need better control about this kind of stuff, i.e. you're dealing with moments in time and not just an amount of minutes and hours, see Basil Bourque's answer below.
tl;dr
Duration.ofMinutes( 260L )
.toString()
PT4H20M
… or …
LocalTime.MIN.plus(
Duration.ofMinutes( 260L )
).toString()
04:20
Duration
The java.time classes include a pair of classes to represent spans of time. The Duration class is for hours-minutes-seconds, and Period is for years-months-days.
Duration d = Duration.ofMinutes( 260L );
Duration parts
Access each part of the Duration by calling to…Part. These methods were added in Java 9 and later.
long days = d.toDaysPart() ;
int hours = d.toHoursPart() ;
int minutes = d.toMinutesPart() ;
int seconds = d.toSecondsPart() ;
int nanos = d.toNanosPart() ;
You can then assemble your own string from those parts.
ISO 8601
The ISO 8601 standard defines textual formats for date-time values. For spans of time unattached to the timeline, the standard format is PnYnMnDTnHnMnS. The P marks the beginning, and the T separates the years-month-days from the hours-minutes-seconds. So an hour and a half is PT1H30M.
The java.time classes use ISO 8601 formats by default for parsing and generating strings. The Duration and Period classes use this particular standard format. So simply call toString.
String output = d.toString();
PT4H20M
For alternate formatting, build your own String in Java 9 and later (not in Java 8) with the Duration::to…Part methods. Or see this Answer for using regex to manipulate the ISO 8601 formatted string.
LocalTime
I strongly suggest using the standard ISO 8601 format instead of the extremely ambiguous and confusing clock format of 04:20. But if you insist, you can get this effect by hacking with the LocalTime class. This works if your duration is not over 24 hours.
LocalTime hackUseOfClockAsDuration = LocalTime.MIN.plus( d );
String output = hackUseOfClockAsDuration.toString();
04:20
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
Where to obtain the java.time classes?
Java SE 8 and SE 9 and later
Built-in.
Part of the standard Java API with a bundled implementation.
Java 9 adds some minor features and fixes.
Java SE 6 and SE 7
Much of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
The ThreeTenABP project adapts ThreeTen-Backport (mentioned above) for Android specifically.
See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.
Use java.text.SimpleDateFormat to convert minute into hours and minute
SimpleDateFormat sdf = new SimpleDateFormat("mm");
try {
Date dt = sdf.parse("90");
sdf = new SimpleDateFormat("HH:mm");
System.out.println(sdf.format(dt));
} catch (ParseException e) {
e.printStackTrace();
}
You can also use the TimeUnit class. You could define private static final String FORMAT = "%02d:%02d:%02d";
can have a method like:
public static String parseTime(long milliseconds) {
return String.format(FORMAT,
TimeUnit.MILLISECONDS.toHours(milliseconds),
TimeUnit.MILLISECONDS.toMinutes(milliseconds) - TimeUnit.HOURS.toMinutes(
TimeUnit.MILLISECONDS.toHours(milliseconds)),
TimeUnit.MILLISECONDS.toSeconds(milliseconds) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(milliseconds)));
}
I use this function for my projects:
public static String minuteToTime(int minute) {
int hour = minute / 60;
minute %= 60;
String p = "AM";
if (hour >= 12) {
hour %= 12;
p = "PM";
}
if (hour == 0) {
hour = 12;
}
return (hour < 10 ? "0" + hour : hour) + ":" + (minute < 10 ? "0" + minute : minute) + " " + p;
}
It can be done like this
int totalMinutesInt = Integer.valueOf(totalMinutes.toString());
int hours = totalMinutesInt / 60;
int hoursToDisplay = hours;
if (hours > 12) {
hoursToDisplay = hoursToDisplay - 12;
}
int minutesToDisplay = totalMinutesInt - (hours * 60);
String minToDisplay = null;
if(minutesToDisplay == 0 ) minToDisplay = "00";
else if( minutesToDisplay < 10 ) minToDisplay = "0" + minutesToDisplay ;
else minToDisplay = "" + minutesToDisplay ;
String displayValue = hoursToDisplay + ":" + minToDisplay;
if (hours < 12)
displayValue = displayValue + " AM";
else
displayValue = displayValue + " PM";
return displayValue;
} catch (Exception e) {
LOGGER.error("Error while converting currency.");
}
return totalMinutes.toString();
(In Kotlin) If you are going to put the answer into a TextView or something you can instead use a string resource:
<string name="time">%02d:%02d</string>
And then you can use this String resource to then set the text at run time using:
private fun setTime(time: Int) {
val hour = time / 60
val min = time % 60
main_time.text = getString(R.string.time, hour, min)
}
Given input in seconds you can transform to format hh:mm:ss like this :
int hours;
int minutes;
int seconds;
int formatHelper;
int input;
//formatHelper maximum value is 24 hours represented in seconds
formatHelper = input % (24*60*60);
//for example let's say format helper is 7500 seconds
hours = formatHelper/60*60;
minutes = formatHelper/60%60;
seconds = formatHelper%60;
//now operations above will give you result = 2hours : 5 minutes : 0 seconds;
I have used formatHelper since the input can be more then 86 400 seconds, which is 24 hours.
If you want total time of your input represented by hh:mm:ss, you can just avoid formatHelper.
I hope it helps.
Here is my function for convert a second,millisecond to day,hour,minute,second
public static String millisecondToFullTime(long millisecond) {
return timeUnitToFullTime(millisecond, TimeUnit.MILLISECONDS);
}
public static String secondToFullTime(long second) {
return timeUnitToFullTime(second, TimeUnit.SECONDS);
}
public static String timeUnitToFullTime(long time, TimeUnit timeUnit) {
long day = timeUnit.toDays(time);
long hour = timeUnit.toHours(time) % 24;
long minute = timeUnit.toMinutes(time) % 60;
long second = timeUnit.toSeconds(time) % 60;
if (day > 0) {
return String.format("%dday %02d:%02d:%02d", day, hour, minute, second);
} else if (hour > 0) {
return String.format("%d:%02d:%02d", hour, minute, second);
} else if (minute > 0) {
return String.format("%d:%02d", minute, second);
} else {
return String.format("%02d", second);
}
}
Testing
public static void main(String[] args) {
System.out.println("60 => " + secondToFullTime(60));
System.out.println("101 => " + secondToFullTime(101));
System.out.println("601 => " + secondToFullTime(601));
System.out.println("7601 => " + secondToFullTime(7601));
System.out.println("36001 => " + secondToFullTime(36001));
System.out.println("86401 => " + secondToFullTime(86401));
}
Output
60 => 1:00
101 => 1:41
601 => 10:01
7601 => 2:06:41
36001 => 10:00:01
86401 => 1day 00:00:01
Hope it help
Minutes mod 60 will gives hours with minutes remaining.
http://www.cafeaulait.org/course/week2/15.html
int mHours = t / 60; //since both are ints, you get an int
int mMinutes = t % 60;
System.out.printf("%d:%02d", "" +mHours, "" +mMinutes);
Solution on kotlin Documentation
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
val min = 150.toDuration(DurationUnit.MINUTES)
val time = min.toComponents { days, hours, minutes, seconds, nanoseconds ->
"$days $hours $minutes $seconds $nanoseconds"
}
We get 0 days 2 hours 30 minutes 0 seconds 0 nanoseconds
We can also use
DurationUnit.DAYS
DurationUnit.HOURS
DurationUnit.SECONDS
DurationUnit.MILLISECONDS
DurationUnit.MICROSECONDS
DurationUnit.NANOSECONDS
long d1Ms=asa.getTime();
long d2Ms=asa2.getTime();
long minute = Math.abs((d1Ms-d2Ms)/60000);
int Hours = (int)minute/60;
int Minutes = (int)minute%60;
stUr.setText(Hours+":"+Minutes);
Try this code:
import java.util.Scanner;
public class BasicElement {
public static void main(String[] args){
Scanner input = new Scanner(System.in);
int hours;
System.out.print("Enter the hours to convert:");
hours =input.nextInt();
int d=hours/24;
int m=hours%24;
System.out.println(d+"days"+" "+m+"hours");
}
}
import java.util.Scanner;
public class Time{
public static void main(String[]args){
int totMins=0;
int hours=0;
int mins=0;
Scanner sc= new Scanner(System.in);
System.out.println("Enter the time in mins: ");
totMins= sc.nextInt();
hours=(int)(totMins/60);
mins =(int)(totMins%60);
System.out.printf("%d:%d",hours,mins);
}
}

Manipulate Date/Time in RDF4J for Debugging

I'm using RDF4J 2.2.1 on Windows 10 Professional 64-bit. I will have some SPIN constructor rules which are sensitive to date/time. For example, I may want to compare a triple containing an xsd:dateTime datatype property to the output of SPARQL's built-in now() function. To debug this functionality, it would be convenient to manipulate RDF4J's perception of date/time somehow rather than manipulating the system clock. I'm aware that there is general commercial software (e.g. Solution Soft's "Time Machine") that can generally manipulate the perception of time for any Windows process. However, this software appears to be far too expensive for our little proof-of-concept project.
What I'd like to be able to do:
Set RDF4J's date/time to some arbitrary date/time value.
Have RDF4J's date/time proceed at real time speed or at some programmable faster speed during debugging.
Does anyone have suggestions for how to manipulate in this manner date/time for RDF4J? It would make my debugging of time-sensitive SPIN rules much more efficient. I'd prefer not to fight my PC's system clock since many other things depend on it. I suppose that running an entire virtual PC and debugging on the virtual PC is another option, but it seems there should be a simpler way.
Thanks.
You could accomplish this by implementing a custom SPARQL function and using that instead of the actual now() function. Call it mock_now() for example. Since you implement it, you have full control over its behavior.
I'm posting my solution to my question in hopes it might help others as a further example of a custom SPARQL function under RDF4J. I don't hold this out as en elegant solution (due to how I set test conditions), but it does work and meets my requirements. This solution extends the answer from #jeen_broekstra based on http://docs.rdf4j.org/custom-sparql-functions/...
I now have a custom implemented in the namespace defined by PREFIX soo: <http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#>as a function called soo:spectrumOpsDateTime() which can take either three or no arguments. The three arguments case allows setting the scaled date time as follows.
First argument: xsd:boolean... use system clock if true or use scaled clock if false
Second argument: xsd:dateTime (ignored if first argument is true)... the starting date/time for scaled clock operation
Third argument: xsd:double (ignored if first argument is true)... the scaled clock rate (e.g. 2.0 means the scaled clock runs faster, at twice real time)
If there are no arguments, soo:spectrumOpsDateTime() returns the scaled date/time or the system date/time depending on what the initial values in the Java code specify or what the last three-argument call specified. The SPARQL and SPIN code under test will use only the no-argument version. Test setup queries will set up the time conditions for particular tests.
Here's an example SPARQL setup query to set up a 2x speed starting this morning:
PREFIX soo: <http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#>
SELECT DISTINCT *
WHERE {
BIND(soo:spectrumOpsDateTime("false"^^xsd:boolean, "2017-08-22T10:49:21.019-05:00"^^xsd:dateTime, "2.0"^^xsd:double) AS ?testDateTime) .
}
Here's an example SPARQL query to get the scaled date/time:
PREFIX soo: <http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#>
SELECT DISTINCT *
WHERE {
BIND(soo:spectrumOpsDateTime() AS ?testDateTime) .
}
The single class used to implement this custom function is:
/**
*
*/
package mil.disa.dso.spo.a2i.nsc.sharing2025.scaledDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
import org.eclipse.rdf4j.query.algebra.evaluation.function.Function;
/**
* Class for generating a configurable date/time clock that can either be a pass-through of the
* system clock or a scaled clock starting at a specified date/time running at a specified
* rate from that specified time (first call).
* #author Greg Cox of Roberson and Associates &copy Copyright 2017 Roberson and Associates, All Right Reserved
*
*/
public class DateTimeGenerator implements Function {
private static final String thisClassName = "RDF4JCustomSPARQLFunction." + DateTimeGenerator.class.getSimpleName();
private static final String thisClassFullName = DateTimeGenerator.class.getName();
private static final boolean errorMessages = true;
private static final boolean verboseMessages = true;
private double clockPace = 2.0; // the speed of the clock, 1.0 is real time, 2.0 is 2x real time (double speed)
private boolean useSystemClock = false; // flag to indicate whether to use scaled clock or pass through the system clock
private ZonedDateTime startingRealDateTime = null; // the real time stamp at the first call to the evaluate function
private ZonedDateTime startingScaledDateTime = // the scaled time stamp (starting scaled time) at the first call to the evaluate function
ZonedDateTime.parse("2016-08-21T17:29:37.568-05:00");
// define a constant for the namespace of custom function
private static String NAMESPACE = "http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#"; // defined as soo: elsewhere
// this is the evaluate function needed to implement the RDF4J Function interface
// it can take 0 or 3 arguments
// 0 - get the current scaled time (starting by first call)
// 3 - useSystemClock flag (true/false), starting date/time (xsd:dateTime), clock pace (non-negative real w/ 1.0 meaning 1sec = 1sec)
#SuppressWarnings("unused")
#Override
public Value evaluate(ValueFactory valueFactory, Value... args) throws ValueExprEvaluationException {
String thisMethodMessagePrefix = "";
if (errorMessages || verboseMessages ) {
String thisMethodName = ".evaluate: ";
thisMethodMessagePrefix = thisClassName + thisMethodName;
}
if (args.length == 3) {
// Three arguments --> attempting to set mode/parameters, so attempt to parse/check them
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "attempting to set scaled clock mode/parameters");
boolean argErrFlag = false;
boolean newUseSystemClock = false;
String argErrMessage = "";
// first argument should be true/false on whether to use system clock (true) or scaled clock (false)
if (!(args[0] instanceof Literal)) {
argErrFlag = true;
argErrMessage += "first argument must be a literal true/false value... ";
} else {
String useSystemClockString = args[0].stringValue();
if (useSystemClockString.equalsIgnoreCase("true")) {
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "use system clock specified");
newUseSystemClock = true;
} else if (useSystemClockString.equalsIgnoreCase("false")) {
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "use scaled clock specified");
newUseSystemClock = false;
}
else {
argErrFlag = true;
argErrMessage += "first argument must be a literal true/false value... ";
}
}
// second argument should be starting date/time for scaled clock (ignore if using system clock)
ZonedDateTime startTime = null;
if (!newUseSystemClock) {
if (!(args[1] instanceof Literal)) {
argErrFlag = true;
argErrMessage += "second argument must be literal xsd:dateTime value for start of scaled date/time... ";
} else {
String startDateTimeString = args[1].stringValue();
try {
startTime = ZonedDateTime.parse(startDateTimeString);
} catch (Exception e) {
argErrFlag = true;
argErrMessage += "could not parse starting date/time... " + e.getMessage() + "... ";
}
}
}
// third argument should be clock pace for scaled clock (ignore if using system clock)
Double newClockPace = null;
if (!newUseSystemClock) {
if (!(args[2] instanceof Literal)) {
argErrFlag = true;
argErrMessage += "third argument must be literal xsd:double value for clock pace... ";
} else {
String clockPaceString = args[2].stringValue();
try {
newClockPace = Double.parseDouble(clockPaceString);
} catch (Exception e) {
argErrFlag = true;
argErrMessage += "could not parse clock pace which should be a positive xsd:double... ";
}
if ((newClockPace != null) && (newClockPace <= 0.0)) {
argErrFlag = true;
argErrMessage += "clock pace must be positive, got " + newClockPace + "... ";
}
}
}
// check for errors and set up the generator if no errors...
if (argErrFlag) {
if (errorMessages) System.err.println(thisMethodMessagePrefix + "ERROR - " + argErrMessage);
if (errorMessages) System.err.println(thisMethodMessagePrefix + "throwing exception...");
throw new ValueExprEvaluationException(
"spectrum operations time function soo:spectrumOpsDateTime() encountered errors in function arguments... " +
argErrMessage);
} else if (newUseSystemClock) {
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "using unscaled system clock");
useSystemClock = newUseSystemClock;
} else if (!newUseSystemClock) {
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "using scaled time");
useSystemClock = newUseSystemClock;
startingRealDateTime = ZonedDateTime.now();
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "setting starting real time to " + startingRealDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "setting start time to " + startTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
startingScaledDateTime = startTime;
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "setting clock pace to " + String.format("%5.2f", newClockPace * 100.0) + "%");
clockPace = newClockPace;
}
} else if (args.length != 0) { // can only have no arguments or three arguments...
throw new ValueExprEvaluationException(
"spectrum operations time function soo:spectrumOpsDateTime() requires "
+ "zero arguments or three arguments, got "
+ args.length + " arguments");
}
// now run the generator and return the result...
IRI xsdDateTimeIRI = valueFactory.createIRI("http://www.w3.org/2001/XMLSchema#dateTime"); // long-form equivalent to xsd:dateTime
if (useSystemClock) {
String unscaledTimeString = millisTrailingZeroes(ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
return valueFactory.createLiteral(unscaledTimeString, xsdDateTimeIRI);
} else {
errString = null;
String scaledTimeString = millisTrailingZeroes(getScaledDateTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
if (scaledTimeString == null) {
if (errorMessages) System.err.println(thisMethodMessagePrefix + "ERROR - scaled time returned null");
if (errorMessages) System.err.println(thisMethodMessagePrefix + "thowing exception...");
throw new ValueExprEvaluationException("could not generate valid scaled time string" + ((errString == null) ? "" : "... " + errString));
}
return valueFactory.createLiteral(scaledTimeString, xsdDateTimeIRI);
}
}
private static String errString = null;
/**
* Utility method to make all the millisecond fields of an <tt>ISO_OFFSET_DATE_TIME</tt> three digits by
* adding trailing zeroes as needed. Why? Because of trouble with various implementations interpreting
* 1 and 2 digit milliseconds differently. Should be standard decimal, but sometimes interpreted
* as number of milliseconds (e.g. .39T interpreted as 39 millieconds inststead of 390 milliseconds)
* #param <tt>ISO_OFFSET_DATE_TIME</tt> string to check for millisecond field length
* #return <tt>ISO_OFFSET_DATE_TIME</tt> strnig with trailing zeroes in milliseconds field
* as require to make the field three digits or <tt>null</tt> on error
*/
private static String millisTrailingZeroes(String isoDateTimeString) {
if (isoDateTimeString == null) {
errString = "DateTimeGenerator.millisTrailingZeroes: got null isoDateTimeString argument, returning null...";
return null;
}
String[] ss_l1 = isoDateTimeString.split("\\."); // Example: 2017-08-18T13:01:05.39-05:00 --> 2017-08-18T13:01:05 AND 39-05:00
if (ss_l1.length != 2) {
errString = "DateTImeGenerator.millisTrailingZeros: first parsing split of isoDateTimeString=" + isoDateTimeString + " by '.' got unexpected number of parts=" + ss_l1.length;
return null;
}
String[] ss_l2 = ss_l1[1].split("-"); // 39-05:00 --> 39 AND 05:00
if (ss_l2.length != 2) {
errString = "DateTImeGenerator.millisTrailingZeros: second parsing split of " + ss_l1[1] + " by '-' got unexpected number of parts=" + ss_l2.length;
return null;
}
if (ss_l2[0].length() == 1) {
ss_l2[0] = ss_l2[0] + "00";
} else if (ss_l2[0].length() == 2)
ss_l2[0] = ss_l2[0] + "0"; // 39 --> 390
return ss_l1[0] + "." + ss_l2[0] + "-" + ss_l2[1]; // 2017-08-18T13:01:05.390-05:00
}
/**
* Method to get the current scaled date time according to the state of this DateTimeGenerator.
* If <tt>useSystemClock</tt> is <tt>true</tt>, then time is not
* scaled and system time is returned instead of scaled time.
* #return scaled date time if <tt>useSystemClock</tt> is <tt>true</tt> or
* system date time if <tt>useSystemClock</tt> is <tt>false</tt>
*/
private ZonedDateTime getScaledDateTime() {
ZonedDateTime scaledDateTime = null;
if (useSystemClock) {
scaledDateTime = ZonedDateTime.now();
} else {
if (startingRealDateTime == null)
startingRealDateTime = ZonedDateTime.now();
long realMillisFromFirstCall = ChronoUnit.MILLIS.between(startingRealDateTime, ZonedDateTime.now());
long scaledMillisFromFirstCall = (long) ((double) realMillisFromFirstCall * clockPace);
scaledDateTime = ChronoUnit.MILLIS.addTo(startingScaledDateTime, scaledMillisFromFirstCall);
}
return scaledDateTime;
}
#Override
public String getURI() {
return NAMESPACE + "spectrumOpsDateTime";
}
/**
* Test main method
* #param args command line arguments (ignored)
*/
#SuppressWarnings("unused")
public static void main(String[] args) {
String thisMethodMessagePrefix = "";
if (errorMessages || verboseMessages ) {
String thisMethodName = ".main: ";
thisMethodMessagePrefix = thisClassName + thisMethodName;
}
DateTimeGenerator testGen = new DateTimeGenerator();
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "custom SPARQL method URI: " + testGen.getURI());
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "fully-qualified class name: " + thisClassFullName);
ValueFactory testVF = SimpleValueFactory.getInstance();
Value testValues[] = new Value[0];
while (true) {
if (verboseMessages) System.out.println(thisMethodMessagePrefix + "scaled: " + testGen.evaluate(testVF, testValues).stringValue() +
" current real: " + millisTrailingZeroes(ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
In my case, the jar file exported from Eclipse executes under my installation of Apache and resides at C:\Apache\apache-tomcat-8.5.15\webapps\rdf4j-server\WEB-INF\lib\ScaledDateTime.jar I restart the Apache server after replacing this jar file when I do mofifications.