Formatting values using FormatString property

I'm trying to format a value based on its FormatString property.

Currently I have the following (as part of a for loop assigning text to a bunch of labels):

self.getComponent('Label '+ str(row)).text = str(round(sortedTags.getValueAt(row,2),1)) + " : " + sortedTags.getValueAt(row,6)

Where sortedTags.getValueAt(row,2) is a tag value and sortedTags.getValueAt(row,6) is a tag name so the label text comes out something like:

52.6 : TT101

I would like to replace the rounding with formatting as defined on the tag because in some cases I want two decimal places instead of one.

If it was an expression I could use the numberFormat function but I'm not sure how to do it in Python :frowning:

However you are generating sortedTags, you will also need to read the format strings. Show all relevant code.

The direct equivalent would be to import java.text.DecimalFormat and run the formatting operation yourself:

To do this purely in python, it would look like this. This will do the rounding and formatting all at once.

self.getComponent('Label'  + str(row)) = "{value:.{d}f} : {name}".format(value = sortedTags.getVAlueAt(row,2),name = sortedTags.getVAlueAt(row,6), d = 2)

And of course you can supply a variable to the value of d and set the number of decimal places you want to show.

No, I'm pretty sure Nick wants to use the format string from the tag, which is not a python-compatible format.

Hi pturmel, see below for the full script. It's called every few seconds on a vertical group of labels to update the text values so they are in descending order.

def sortTags(self):
	"""
	Arguments:
		self: A reference to the component instance this method is invoked on. This argument
		  is automatic and should not be specified when invoking this method.
	"""
	# Set up the empty lists we need
	names = []
	colors = []
	paths = []
	valuePaths = []
	tooltipPaths = []
	engUnitPaths = []
	displayNames = []

	# Scan through the rows of pens from the chart
	# and put the path for the tooltips, values etc. into seperate datasets
	rawNames = self.inputTags.getColumnAsList(0)
	rawDisplayNames = self.inputTags.getColumnAsList(1)
	rawColors = self.inputTags.getColumnAsList(2)
	
	for index, item in enumerate(rawNames):
		if not rawNames[index]:
			break
		names.append(rawNames[index])
		if rawDisplayNames[index]:
			displayNames.append(rawDisplayNames[index])
		else:
			displayNames.append(rawNames[index])
		colors.append(str(rawColors[index]))
		paths.append("[default]GlobalVars/" + rawNames[index])
		
		valuePaths.append("[default]GlobalVars/" + rawNames[index] + ".Value")		
		tooltipPaths.append("[default]GlobalVars/" + rawNames[index] + ".Tooltip")
		engUnitPaths.append("[default]GlobalVars/" + rawNames[index] + ".EngUnit")

	# Create a dataset with the unsorted tag names
	unsortedTags = system.dataset.toDataSet(["Name"], [[name] for name in names])
			
	# If the list of names is not empty then look up the actual tool tip, value and eng units from the paths on each row
	if len(names) > 0:
		toolTips = [toolTipTag.value for toolTipTag in system.tag.readBlocking(tooltipPaths)]
		values = [valueTag.value for valueTag in system.tag.readBlocking(valuePaths)]
		engUnits = [engUnitTag.value for engUnitTag in system.tag.readBlocking(engUnitPaths)]
	
		# Add the various lists to the unsortedTags dataset as individual columns
		unsortedTags = system.dataset.addColumn(unsortedTags, 1, paths, "Path", str)	
		unsortedTags = system.dataset.addColumn(unsortedTags, 2, values, "Value", float)	
		unsortedTags = system.dataset.addColumn(unsortedTags, 3, engUnits, "Eng Unit", str)	
		unsortedTags = system.dataset.addColumn(unsortedTags, 4, toolTips, "Tooltip", str)
		unsortedTags = system.dataset.addColumn(unsortedTags, 5, colors, "Color", str)
		unsortedTags = system.dataset.addColumn(unsortedTags, 6, displayNames, "Display Name", str)

		# Sort the data set by Value in decending order
		sortedTags = system.dataset.sort(unsortedTags, "Value", False)
	
	#For each row in the sorted data set update the value, eng units, tooltip and colour
	for row in range(len(rawNames)):
		if row >= unsortedTags.getRowCount():
			self.getComponent('Label '+ str(row)).visible = False
		else:
			if sortedTags.getValueAt(row,2) == None:
				self.getComponent('Label '+ str(row)).text = "- : " + sortedTags.getValueAt(row,6)
			else:
				self.getComponent('Label '+ str(row)).text = str(round(sortedTags.getValueAt(row,2),1)) + " : " + sortedTags.getValueAt(row,6)
		
			self.getComponent('Label '+ str(row)).toolTipText = sortedTags.getValueAt(row,0) + " - " + sortedTags.getValueAt(row,4) + " (" + sortedTags.getValueAt(row,3) + ")"
			self.getComponent('Label '+ str(row)).foreground = system.gui.color(sortedTags.getValueAt(row,5))
			self.getComponent('Label '+ str(row)).visible = True

Yes, I want to use the FormatString property from each tag to decide how many decimal places to use. The script belongs to a template that is used in multiple places with different sets of tags and the number of decimal places required will vary between tags.

That's a relatively expensive script to run every few seconds...
Some general improvements:

	rawNames = self.inputTags.getColumnAsList(0)
	rawDisplayNames = self.inputTags.getColumnAsList(1)
	rawColors = self.inputTags.getColumnAsList(2)

This is looping through all the rows in the dataset three separate times. It's slightly more code to write, but O(1) is better than O(3) - loop the input dataset only once.

Ok, cool, we want both the index and the item, so we're using enumerate...

Huh? This will never be the case, because you can just use item to refer to rawNames[index] - that's the whole point of enumerate.

This doesn't actually safeguard you against an IndexError if you don't have a value at index in rawDisplayNames.

These should really be one list, or a list of three lists, or something. It's easy to write a function that can "chunk" the return values of readBlocking into groups of 3, or groups of rowCount, or whatever, and the efficiency improvement of only issuing one readBlocking call is worth it.

map(list, names) is equivalent, but that's more personal preference :golfing_man:

This is perhaps more of a stylistic choice, but:
I would recommend separating this function into at least two pieces. One piece should be "given this input dataset, return this output dataset" and that's it. No side effects, no UI state updates - just dataset in, dataset out. That function should live in the project library :slight_smile:, or at least as a "pure" custom method on the component/container.

The second piece should be the "extract dataset A out of a component, pass it to that well tested script library function, and then perform UI updates accordingly". That can sometimes be worth breaking up further; e.g. you could just broadcast a "something updated" message and have your individual UI elements react accordingly.

1 Like

As for the specific question in the OP, ignoring my own advice about batching readBlocking calls together, you could do it with something like this:

		from java.text import DecimalFormat # make sure you do this at the highest level possible, not inside of a loop. Again, project library :)
		formatter = DecimalFormat()
		def formatValue(value, pattern):
			formatter.applyPattern(pattern)
			return formatter.format(value)
		values = [
			formatValue(valueTag.value, formatTag.value)
			for valueTag, formatTag in zip(system.tag.readBlocking(valuePaths), system.tag.readBlocking(formatPaths))
		]

You need to add formatStringPaths just like you are handling tool tips and engineering units. Then you'll have the format string in a row with tag name, value, and tooltip. Then, in your formatting loop, you can pass that format string to the DecimalFormat class that Paul linked.

Ok, big thanks to PGriffith and pturmel for their help. My current code is as follows. The formatting works but I still need to digest and incorporate the additional tips from PGriffith. I'm pretty new to Python so the suggestions for improving the efficiency and quality of my code are very much appreciated!

def sortTags(self):
	"""
	Arguments:
		self: A reference to the component instance this method is invoked on. This argument
		  is automatic and should not be specified when invoking this method.
	"""
	from java.text import DecimalFormat
	
	# Set up the empty lists we need
	names = []
	colors = []
	paths = []
	valuePaths = []
	tooltipPaths = []
	engUnitPaths = []
	formatStringPaths = []
	displayNames = []

	# Scan through the rows of pens from the chart
	# and put the path for the tooltips, values etc. into seperate datasets
	rawNames = self.inputTags.getColumnAsList(0)
	rawDisplayNames = self.inputTags.getColumnAsList(1)
	rawColors = self.inputTags.getColumnAsList(2)
	
	for index, item in enumerate(rawNames):
		if not rawNames[index]:
			break
		names.append(rawNames[index])
		if rawDisplayNames[index]:
			displayNames.append(rawDisplayNames[index])
		else:
			displayNames.append(rawNames[index])
		colors.append(str(rawColors[index]))
		paths.append("[default]GlobalVars/" + rawNames[index])
		
		valuePaths.append("[default]GlobalVars/" + rawNames[index] + ".Value")		
		tooltipPaths.append("[default]GlobalVars/" + rawNames[index] + ".Tooltip")
		engUnitPaths.append("[default]GlobalVars/" + rawNames[index] + ".EngUnit")
		formatStringPaths.append("[default]GlobalVars/" + rawNames[index] + ".FormatString")

	# Create a dataset with the unsorted tag names
	unsortedTags = system.dataset.toDataSet(["Name"], [[name] for name in names])
			
	# If the list of names is not empty then look up the actual tool tip, value and eng units from the paths on each row
	if len(names) > 0:
		toolTips = [toolTipTag.value for toolTipTag in system.tag.readBlocking(tooltipPaths)]
		values = [valueTag.value for valueTag in system.tag.readBlocking(valuePaths)]
		engUnits = [engUnitTag.value for engUnitTag in system.tag.readBlocking(engUnitPaths)]
		formatString = [formatStringTag.value for formatStringTag in system.tag.readBlocking(formatStringPaths)]
	
		# Add the various lists to the unsortedTags dataset as individual columns
		unsortedTags = system.dataset.addColumn(unsortedTags, 1, paths, "Path", str)	
		unsortedTags = system.dataset.addColumn(unsortedTags, 2, values, "Value", float)	
		unsortedTags = system.dataset.addColumn(unsortedTags, 3, engUnits, "Eng Unit", str)	
		unsortedTags = system.dataset.addColumn(unsortedTags, 4, toolTips, "Tooltip", str)
		unsortedTags = system.dataset.addColumn(unsortedTags, 5, colors, "Color", str)
		unsortedTags = system.dataset.addColumn(unsortedTags, 6, displayNames, "Display Name", str)
		unsortedTags = system.dataset.addColumn(unsortedTags, 7, formatString, "Format String", str)

		# Sort the data set by Value in decending order
		sortedTags = system.dataset.sort(unsortedTags, "Value", False)
	
	#For each row in the sorted data set update the value, eng units, tooltip and colour
	for row in range(len(rawNames)):
		if row >= unsortedTags.getRowCount():
			self.getComponent('Label '+ str(row)).visible = False
		else:
			if sortedTags.getValueAt(row,2) == None:
				self.getComponent('Label '+ str(row)).text = "- : " + sortedTags.getValueAt(row,6)
			else:
				formatter = DecimalFormat(sortedTags.getValueAt(row,7))
				self.getComponent('Label '+ str(row)).text = str(formatter.format(sortedTags.getValueAt(row,2))) + " : " + sortedTags.getValueAt(row,6)

			self.getComponent('Label '+ str(row)).toolTipText = sortedTags.getValueAt(row,0) + " - " + sortedTags.getValueAt(row,4) + " (" + sortedTags.getValueAt(row,3) + ")"
			self.getComponent('Label '+ str(row)).foreground = system.gui.color(sortedTags.getValueAt(row,5))
			self.getComponent('Label '+ str(row)).visible = True

Hi PGriffith,
Could you please elaborate on the below point a bit further. I see why looping through the dataset three times is inefficient but I'm not sure how to do it correctly. Are you able to give me an example?

It's a microoptimization, to be fair. On a list with less than thousands of items, it's probably faster to call getColumnAsList repeatedly.

But the theoretical tradeoff would be:

rawNames = []
rawDisplayNames = []
rawColors = []

for i in xrange(self.inputTags.rowCount):
	rawNames.append(self.inputTags.getValueAt(i, 0))
	rawDisplayNames.append(self.inputTags.getValueAt(i, 1))
	rawColors.append(self.inputTags.getValueAt(i, 2))

The real-world tradeoff of Jython/the JVM having to periodically grow the input lists into new chunks of memory (since Jython doesn't have an easy way to preallocate a typed list the way Java does) almost certainly makes this method slower on real-world data, like I said - up until the tens or hundreds of thousands of rows, at which point Perspective's property model is going to be falling over anyways, so it's a moot point.

I would treat this is an intellectual point but not actually follow this point of advice in practice :slight_smile: