Can We Make Jython Date Arithmetic More Pythonic?

Ignition has no concept of "Duration" in its type system (I'm not counting datetimes that you treat like durations), but Java does. For this reason, I like using the java.time.Duration class for all my duration needs.

The Duration class has everything I need, but time arithmetic (with both java.util.Date, Ignition's datetime type, and java.time.Duration) still has to be done with functions instead of operators. I want to be able to write my_date = my_prev_date + my_duration, and weighted_duration = first_duration * 0.75 + second_duration * 0.25.

My question is this:
If I import TimeUtils at the top of any script in my Ignition project, will I then be able to use operators to perform my java.time.Duration calculations?

My concern is that I will either be unable to write these properties to the imported Jython objects, or that those properties could get dropped between different executions from the same script (scripts are loaded/ran once, but their function definitions may then be used elsewhere).

# Module: TimeUtils
from java.time import Duration
from java.util import Date

NANOS_IN_SEC = 1000000000.0
NANOS_IN_MILLIS = 1000000.0

Duration.__neg__ = Duration.negated
Duration.__abs__ = Duration.abs

def duration__add__(self, other):
	if type(other) is Duration:
		pass
	elif type(other) is Date:
		pass
	else:
		pass

Duration.__add__ = Duration.plus
Duration.__sub__ = Duration.minus

def duration__str__(self):
	sign = '-' if self.isNegative() else ''
	pos = abs(self) #take the positive to avoid conjugates
	hours = pos.toHours()
	minutes = pos.toMinutesPart()
	seconds = pos.toSecondsPart()
	nanoseconds = pos.toNanosPart()
	return '{}{:0>2d}:{:0>2d}:{:0>2d}.{:0>9d}' \
		.format(sign, hours, minutes, seconds, nanoseconds)

Duration.__str__ = duration__str__

def duration__repr__(self):
	return 'Duration({})'.format(str(self))

Duration.__repr__ = duration__repr__

def duration__mul__(self, other):
	
	if type(other) in [int, long, float]:
		
		secs = self.toSeconds() * other
		nanos = self.toNanosPart() * other
		
		extra_secs = secs % 1
		nanos += NANOS_IN_SEC * extra_secs
		secs -= extra_secs
		
		if nanos > NANOS_IN_SEC:
			secs += nanos // NANOS_IN_SEC
			nanos %= NANOS_IN_SEC
		elif nanos < -NANOS_IN_SEC:
			secs -= -nanos // NANOS_IN_SEC
			nanos = -(-nanos % NANOS_IN_SEC)

		return Duration.ofSeconds(long(secs), int(nanos))
		
	else:
		raise TypeError("Must multiply by a number")
		
Duration.__mul__ = duration__mul__
	
def duration__div__(self, other):
	return self * (1.0 / other)
	
Duration.__div__ = duration__div__



def date__add__(self, other):
	if type(other) is Duration:
		return system.date.addMillis(self, int(other.toNanos() / NANOS_IN_MILLIS))
	else:
		raise TypeError('can only add a Duration to a date')

Date.__add__ = date__add__

def date__sub__(self, other):
	if type(other) is Duration:
		return system.date.addMillis(self, int((-other).toNanos() / NANOS_IN_MILLIS))
	elif type(other) is Date:
		millisDiff = system.date.millisBetween(other, self)
		return Duration.ofMillis(millisDiff)
	else:
		raise TypeError('can only add a Duration to a date')

Date.__sub__ = date__sub__

You should write this at the top most level of a project script, and then call into those scripting functions from everywhere else. The whole idea of a project script is that it is Project Global, there is no need to import that in other places.

As long as (with addition for example) there is an __add__ function declared, then you should be able to use the + operator with that type.

This should probably be done in java with a custom python wrapper. Not really trivial.

1 Like

I'm a little confused. Are you saying that whenever I have a script on a project resource, like a Perspective component property binding or change script, it uses the project context that's shared between all other scripts in the project?

I'd like to understand more about how project libraries execute. When I modify the java.time.Duration class in a Gateway Scripting Project Library Module, is that the same java.time.Duration class definition that would be used in a Perspective Binding script? My hope is yes, but my guess is no.

Is the java.time.Duration class definition ever updated without triggering a full Project Library refresh?

When an import statement runs import TimeUtils, does that run the TimeUtils.py project library file before executing the rest of the code, or is there a shortcut, a way of preloading libraries into the script context before running?

Maybe I just need to learn more about how python executes in general, but I also wonder how specific some of these questions are to Ignition in particular. Any clarification is appreciated.

Do you mean that I would need to create a custom module to properly implement this?

Yes.

1 Like

What I am saying is, that project script resources are project global.

So on a Perspective component property binding script transform instead of doing something like:

import TimeUtils

#multiple lines of code

You should instead do:

timeUtils.getDurration() #single line call into project script.

Yes. Although some thread locals that Ignition uses for Perspective will only be present when called from a Perspective event. When the project library is called from a pure gateway event, those thread locals won't exist, and system.perspective.* won't work.

They operate like python site packages (mostly), but auto-imported by name into project scopes. That they are imported is important, because it means they only execute top-to-bottom once per project startup (plus project restarts after edits). The auto-import occurs within the context as soon as any event anywhere in that project references any name within that library script.

TimeUtils in one project in the gateway won't be the same java object as TimeUtils created in another project. Only with a 3rd party module could you uniformly make them truly the same.

And keep in mind that project libraries' scope is also confined to the JVM in which they are running--Vision Client and Designer scopes are separate JVMs, so separate namespaces, too.

4 Likes