Related
I am trying to get the RGB values of a CSS color string and wonder how good my code is:
object Color {
def stringToInts(colorString: String): Option[(Int, Int, Int)] = {
val trimmedColorString: String = colorString.trim.replaceAll("#", "")
val longColorString: Option[String] = trimmedColorString.length match {
// allow only strings with either 3 or 6 letters
case 3 => Some(trimmedColorString.flatMap(character => s"$character$character"))
case 6 => Some(trimmedColorString)
case _ => None
}
val values: Option[Seq[Int]] = longColorString.map(_
.foldLeft(Seq[String]())((accu, character) => accu.lastOption.map(_.toSeq) match {
case Some(Seq(_, _)) => accu :+ s"$character" // previous value is complete => start with succeeding
case Some(Seq(c)) => accu.dropRight(1) :+ s"$c$character" // complete the previous value
case _ => Seq(s"$character") // start with an incomplete first value
})
.flatMap(hexString => scala.util.Try(Integer.parseInt(hexString, 16)).toOption)
// .flatMap(hexString => try {
// Some(Integer.parseInt(hexString, 16))
// } catch {
// case _: Exception => None
// })
)
values.flatMap(values => values.size match {
case 3 => Some((values.head, values(1), values(2)))
case _ => None
})
}
}
// example:
println(Color.stringToInts("#abc")) // prints Some((170,187,204))
You may run that example on https://scastie.scala-lang.org
The parts of that code I am most unsure about are
the match in the foldLeft (is it a good idea to use string interpolation or can the code be written shorter without string interpolation?)
Integer.parseInt in conjunction with try (can I use a prettier alternative in Scala?) (solved thanks to excellent comment by Xavier Guihot)
But I expect most parts of my code to be improvable. I do not want to introduce new libraries in addition to com.itextpdf to shorten my code, but using com.itextpdf functions is an option. (The result of stringToInts is going to be converted into a new com.itextpdf.kernel.colors.DeviceRgb(...), thus I have installed com.itextpdf anyway.)
Tests defining the expected function:
import org.scalatest.{BeforeAndAfterEach, FunSuite}
class ColorTest extends FunSuite with BeforeAndAfterEach {
test("shorthand mixed case color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#Fa#F")
val expected = (255, 170, 255)
assert(actual === Some(expected))
}
test("mixed case color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a06")
val expected = (29, 154, 6)
assert(actual === Some(expected))
}
test("too short long color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a6")
assert(actual === None)
}
test("too long shorthand color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a")
assert(actual === None)
}
test("invalid color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9g06")
assert(actual === None)
}
}
At the moment of writing this answer the other answers don't properly handle rgb(), rgba() and named colors cases. Color strings that start with hashes (#) are only a part of the deal.
As you have iText7 as a dependency and iText7 has a pdfHTML add-on which means the logic for parsing CSS colors obviously must be somewhere in iText7 and, more importantly, it must handle various range of CSS color cases. The question is only about finding the right place. Fortunately, this API is public and easy to use.
The method you are interested in is WebColors.getRGBAColor() from package com.itextpdf.kernel.colors which accepts a CSS color string a returns a 4-element array with R, G, B, A values (last one stands for alpha, i.e. transparency).
You can use those values to create a color right away (code in Java):
float[] rgbaColor = WebColors.getRGBAColor("#ababab");
Color color = new DeviceRgb(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
In Scala it must be something like
val rgbaColor = WebColors.getRGBAColor("#ababab");
val color = new DeviceRgb(rgbaColor(0), rgbaColor(1), rgbaColor(2));
I came up with this fun answer (untested); I guess the biggest help for you will be the use of sliding(2,2) instead of the foldLeft.
def stringToInts(colorString: String): Option[(Int, Int, Int)] = {
val trimmedString: String => String = _.trim.replaceAll("#", "")
val validString: String => Option[String] = s => s.length match {
case 3 => Some(s.flatMap(c => s"$c$c"))
case 6 => Some(s)
case _ => None
}
val hex2rgb: String => List[Option[Int]] = _.sliding(2, 2).toList
.map(hex => Try(Integer.parseInt(hex, 16)).toOption)
val listOpt2OptTriple: List[Option[Int]] => Option[(Int, Int, Int)] = {
case Some(r) :: Some(g) :: Some(b) :: Nil => Some(r, g, b)
case _ => None
}
for {
valid <- validString(trimmedString(colorString))
rgb = hex2rgb(valid)
answer <- listOpt2OptTriple(rgb)
} yield answer
}
Here is a possible implementation of your function
def stringToInts(css: String): Option[(Int, Int, Int)] = {
def cssColour(s: String): Int = {
val v = Integer.parseInt(s, 16)
if (s.length == 1) v*16 + v else v
}
val s = css.trim.replaceAll("#", "")
val l = s.length/3
if (l > 2 || l*3 != s.length) {
None
} else {
Try{
val res = s.grouped(l).map(cssColour).toSeq
(res(0), res(1), res(2))
}.toOption
}
}
The implementation would be cleaner if it returned Option[List[Int]] or even Try[List[Int]] to preserve the error in the case of failure.
If you're looking for conciseness, perhaps this solution will do the job (at the expense of efficiency—more on that later):
import scala.util.Try
def parseLongForm(rgb: String): Try[(Int, Int, Int)] =
Try {
rgb.replace("#", "").
grouped(2).toStream.filter(_.length == 2).
map(Integer.parseInt(_, 16)) match { case Stream(r, g, b) => (r, g, b) }
}
def parseShortForm(rgb: String): Try[(Int, Int, Int)] =
parseLongForm(rgb.flatMap(List.fill(2)(_)))
def parse(rgb: String): Option[(Int, Int, Int)] =
parseLongForm(rgb).orElse(parseShortForm(rgb)).toOption
In terms of conciseness is that every function here is effectively a one-liner (if that's something you're looking for right now).
The core is the function parseLongForm, which attempts to parse the long 6-character long form by:
removing the # character
grouping the characters in pairs
filtering out lone items (in case we have an odd number of characters)
parsing each pair
matching with the expected result to extract individual items
parseLongForm represents the possibility of failure with Try, which allows us to fail gracefully whenever parseInt or the pattern matching fails.
parse invokes parseLongForm and, if the result is a failure (orElse), invokes parseShortForm, which just tries the same approach after doubling each character.
It successfully passes the tests that you provided (kudos, that makes addressing the question much easier).
The main issue with this approach is that you would still try to parse the long form even if it can be clear from the beginning that it would not work. So, this is not recommended code if this could be a performance bottleneck for your use case. Another issue is that, although that's more or less hidden, we're using exceptions for flow control (which also hurts performance).
The nice things are conciseness and, I'd argue, readability (as I'd say that the code maps in a fairly straightforward fashion to the problem—but readability, of course, is by definition in the eye of the beholder).
You can find this solution on Scastie.
I'm using pytest.mark to give my tests kwargs. However, if I use the same mark on both the class and a test within the class, the class's mark overrides the mark on the function when the same kwargs are used for both.
import pytest
animal = pytest.mark.animal
#animal(species='croc') # Mark the class with a kwarg
class TestClass(object):
#animal(species='hippo') # Mark the function with new kwarg
def test_function(self):
pass
#pytest.fixture(autouse=True) # Use a fixture to inspect my function
def animal_inspector(request):
print request.function.animal.kwargs # Show how the function object got marked
# prints {'species': 'croc'} but the function was marked with 'hippo'
Where'd my hippo go and how can I get him back?
There are unfortunately various pytest bugs related to this, I'm guessing you're running into one of them. The ones I found are related to subclassing which you don't do there though.
So I've been digging around in the pytest code and figured out why this is happening. The marks on the functions are applied to the function at import time but the class and module level marks don't get applied on the function level until test collection. Function marks happen first and add their kwargs to the function. Then class marks overwrite any same kwargs and module marks further overwrite any matching kwargs.
My solution was to simply create my own modified MarkDecorator that filters kwargs before they are added to the marks. Basically, whatever kwarg values get set first (which seems to always be by a function decorator) will always be the value on the mark. Ideally I think this functionality should be added in the MarkInfo class but since my code wasn't creating instances of that I went with what I was creating instances of: MarkDecorator. Note that I only change two lines from the source code (the bits about keys_to_add).
from _pytest.mark import istestfunc, MarkInfo
import inspect
class TestMarker(object): # Modified MarkDecorator class
def __init__(self, name, args=None, kwargs=None):
self.name = name
self.args = args or ()
self.kwargs = kwargs or {}
#property
def markname(self):
return self.name # for backward-compat (2.4.1 had this attr)
def __repr__(self):
d = self.__dict__.copy()
name = d.pop('name')
return "<MarkDecorator %r %r>" % (name, d)
def __call__(self, *args, **kwargs):
""" if passed a single callable argument: decorate it with mark info.
otherwise add *args/**kwargs in-place to mark information. """
if args and not kwargs:
func = args[0]
is_class = inspect.isclass(func)
if len(args) == 1 and (istestfunc(func) or is_class):
if is_class:
if hasattr(func, 'pytestmark'):
mark_list = func.pytestmark
if not isinstance(mark_list, list):
mark_list = [mark_list]
mark_list = mark_list + [self]
func.pytestmark = mark_list
else:
func.pytestmark = [self]
else:
holder = getattr(func, self.name, None)
if holder is None:
holder = MarkInfo(
self.name, self.args, self.kwargs
)
setattr(func, self.name, holder)
else:
# Don't set kwargs that already exist on the mark
keys_to_add = {key: value for key, value in self.kwargs.items() if key not in holder.kwargs}
holder.add(self.args, keys_to_add)
return func
kw = self.kwargs.copy()
kw.update(kwargs)
args = self.args + args
return self.__class__(self.name, args=args, kwargs=kw)
# Create my Mark instance. Note my modified mark class must be imported to be used
animal = TestMarker(name='animal')
# Apply it to class and function
#animal(species='croc') # Mark the class with a kwarg
class TestClass(object):
#animal(species='hippo') # Mark the function with new kwarg
def test_function(self):
pass
# Now prints {'species': 'hippo'} Yay!
I'm trying to take a large file and split it into many smaller files. The location where each split occurs is based on a predicate returned from examining the contents of each given line (isNextObject function).
I have attempted to read in the large file via the File.ReadLines function so that I can iterate through the file one line at a time without having to hold the entire file in memory. My approach was to group the sequence into a sequence of smaller sub-sequences (one per file to be written out).
I found a useful function that Tomas Petricek created on fssnip called groupWhen. This function worked great for my initial testing on a small subset of the file, but a StackoverflowException is thrown when using the real file. I am not sure how to adjust the groupWhen function to prevent this (I'm still an F# greenie).
Here is a simplified version of the code showing only the relevant parts that will recreate the StackoverflowExcpetion::
// This is the function created by Tomas Petricek where the StackoverflowExcpetion is occuring
module Seq =
/// Iterates over elements of the input sequence and groups adjacent elements.
/// A new group is started when the specified predicate holds about the element
/// of the sequence (and at the beginning of the iteration).
///
/// For example:
/// Seq.groupWhen isOdd [3;3;2;4;1;2] = seq [[3]; [3; 2; 4]; [1; 2]]
let groupWhen f (input:seq<_>) = seq {
use en = input.GetEnumerator()
let running = ref true
// Generate a group starting with the current element. Stops generating
// when it founds element such that 'f en.Current' is 'true'
let rec group() =
[ yield en.Current
if en.MoveNext() then
if not (f en.Current) then yield! group() // *** Exception occurs here ***
else running := false ]
if en.MoveNext() then
// While there are still elements, start a new group
while running.Value do
yield group() |> Seq.ofList }
This is the gist of the code making use Tomas' function:
module Extractor =
open System
open System.IO
open Microsoft.FSharp.Reflection
// ... elided a few functions include "isNextObject" which is
// a string -> bool (examines the line and returns true
// if the string meets the criteria to that we are at the
// start of the next inner file)
let writeFile outputDir file =
// ... write out "file" to the file system
// NOTE: file is a seq<string>
let writeFiles outputDir (files : seq<seq<_>>) =
files
|> Seq.iter (fun file -> writeFile outputDir file)
And here is the relevant code in the console application that makes use of the functions:
let lines = inputFile |> File.ReadLines
writeFiles outputDir (lines |> Seq.groupWhen isNextObject)
Any ideas on the proper way to stop groupWhen from blowing the stack? I'm not sure how I would convert the function to use an accumulator (or to use a continuation instead, which I think is the correct terminology).
The problem with this is that the group() function returns a list, which is an eagerly evaluated data structure, which means that every time you call group() it has to run to the end, collect all results in a list, and return the list. This means that the recursive call happens within that same evaluation - i.e. truly recursively, - thus creating stack pressure.
To mitigate this problem, you could just replace the list with a lazy sequence:
let rec group() = seq {
yield en.Current
if en.MoveNext() then
if not (f en.Current) then yield! group()
else running := false }
However, I would consider less drastic approaches. This example is a good illustration of why you should avoid doing recursion yourself and resort to ready-made folds instead.
For example, judging by your description, it seems that Seq.windowed may work for you.
It's easy to overuse sequences in F#, IMO. You can accidentally get stack overflows, plus they are slow.
So (not actually answering your question),
personally I would just fold over the seq of lines using something like this:
let isNextObject line =
line = "---"
type State = {
fileIndex : int
filename: string
writer: System.IO.TextWriter
}
let makeFilename index =
sprintf "File%i" index
let closeFile (state:State) =
//state.writer.Close() // would use this in real code
state.writer.WriteLine("=== Closing {0} ===",state.filename)
let createFile index =
let newFilename = makeFilename index
let newWriter = System.Console.Out // dummy
newWriter.WriteLine("=== Creating {0} ===",newFilename)
// create new state with new writer
{fileIndex=index + 1; writer = newWriter; filename=newFilename }
let writeLine (state:State) line =
if isNextObject line then
/// finish old file here
closeFile state
/// create new file here and return updated state
createFile state.fileIndex
else
//write the line to the current file
state.writer.WriteLine(line)
// return the unchanged state
state
let processLines (lines: string seq) =
//setup
let initialState = createFile 1
// process the file
let finalState = lines |> Seq.fold writeLine initialState
// tidy up
closeFile finalState
(Obviously a real version would use files rather than the console)
Yes, it is crude, but it is easy to reason about, with
no unpleasant surprises.
Here's a test:
processLines [
"a"; "b"
"---";"c"; "d"
"---";"e"; "f"
]
And here's what the output looks like:
=== Creating File1 ===
a
b
=== Closing File1 ===
=== Creating File2 ===
c
d
=== Closing File2 ===
=== Creating File3 ===
e
f
=== Closing File3 ===
I want to add custom rows in QFileSystemModel under QTreeview. The row is only added when the directory contains files with a certain extension. Basically, After starting up the directory listing, the user will click through the folders. As soon as the folder the user clicked contains the target file, I would like to hide these files (which I know how to do), then use custom rows to represent a summary of these files.
For example, if the folder contains files like the following
A.01.dat
A.02.dat
A.03.dat
...
B.01.dat
B.02.dat
B.03.dat
I would like to create custom rows:
A
B
However, if the folder clicked does not contain these .dat files, then no custom rows should be created.
I have also tried to insert rows directly into QFileSystemModel
self.treeivew.model = QtGui.QFileSystemModel()
...
for n, s in enumerate(self.sequence):
self.treeview.model.beginInsertRows(index, 0, 0)
result = self.treeview.model.insertRow(1, index)
print(result)
self.treeview.model.setData(index, QString(s['Name']),role=QtCore.Qt.DisplayRole)
self.treeview.model.endInsertRows()
But the insertion failed.
If reimplementation is necessary, as I have seen many places have suggested, can anyone provide a concrete example on how the reimplementation should be done to allow such conditional custom row insertion?
Thanks in advance.
I would implement an item model with dynamic child insertion. This is just a standard QAbstractItemModel with a few extra methods -
rowCount - you would normally implement this for a tree model anyway. Just make sure that it returns 0 if the node has children that have not been loaded yet.
hasChildren - override to return True for nodes that have children that haven't been loaded yet and return whatever the base class returns in all other cases.
canFetchMore - return True if the node has children that haven't been loaded yet, False otherwise.
fetchMore - this is where you perform whatever logic you need to decide what nodes to create and insert them into the model.
Here's the basic idea - for nodes that you know have children that haven't been loaded, return 0 from rowCount and True from canFetchMore and hasChildren. This tells Qt to show a node with an expander next to it even though it currently has no children. When the expander is clicked, fetchMore is called and you populate the children from the given parent.
One thing to note - you must call beginInsertRows and endInsertRows in the fetchMore method. What's more, you musn't change the underlying datastore before calling beginInsertRows or after endInsertRows. Unfortunately, you need to know how many rows you are inserting when you call beginInsertRows - so you are probably going to want to generate a list of nodes to add, then make the call to beginInsertRows. If you do it this way though, you can't set the new nodes' parent, as it would change the underlying datastore.
You can see in the code below, that I set the parent node in the Node.insert_child method which is called between the beginInsertRows and endInsertRows calls.
The code doesn't do exactly what you are after - it's a basic file system model illustrating dynamic loading, you'll need to insert you custom logic to generate the category nodes you want in the fetchMore call. It also only shows the filename and lacks icons.
If you want the modified date and size to be shown, you'll need to store these in the relevant nodes and set the model columnCount method to return the correct number of columns.
For icons, extend the model data method to check for the Qt.DecorationRole and return the relevant QIcon.
There might be some superfluous stuff in the code as it's a cut down and repurposed model from something else.
import sys
import os
import sip
sip.setapi('QVariant', 2)
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Node(object):
def __init__(self, name, path=None, parent=None):
super(Node, self).__init__()
self.name = name
self.children = []
self.parent = parent
self.is_dir = False
self.path = path
self.is_traversed = False
if parent is not None:
parent.add_child(self)
def add_child(self, child):
self.children.append(child)
child.parent = self
def insert_child(self, position, child):
if position < 0 or position > self.child_count():
return False
self.children.insert(position, child)
child.parent = self
return True
def child(self, row):
return self.children[row]
def child_count(self):
return len(self.children)
def row(self):
if self.parent is not None:
return self.parent.children.index(self)
return 0
class FileSystemTreeModel(QAbstractItemModel):
FLAG_DEFAULT = Qt.ItemIsEnabled | Qt.ItemIsSelectable
def __init__(self, root, path='c:/', parent=None):
super(FileSystemTreeModel, self).__init__()
self.root = root
self.parent = parent
self.path = path
for file in os.listdir(path):
file_path = os.path.join(path, file)
node = Node(file, file_path, parent=self.root)
if os.path.isdir(file_path):
node.is_dir = True
def getNode(self, index):
if index.isValid():
return index.internalPointer()
else:
return self.root
## - dynamic row insertion starts here
def canFetchMore(self, index):
node = self.getNode(index)
if node.is_dir and not node.is_traversed:
return True
return False
## this is where you put custom logic for handling your special nodes
def fetchMore(self, index):
parent = self.getNode(index)
nodes = []
for file in os.listdir(parent.path):
file_path = os.path.join(parent.path, file)
node = Node(file, file_path)
if os.path.isdir(file_path):
node.is_dir = True
nodes.append(node)
self.insertNodes(0, nodes, index)
parent.is_traversed = True
def hasChildren(self, index):
node = self.getNode(index)
if node.is_dir:
return True
return super(FileSystemTreeModel, self).hasChildren(index)
def rowCount(self, parent):
node = self.getNode(parent)
return node.child_count()
## dynamic row insert ends here
def columnCount(self, parent):
return 1
def flags(self, index):
return FileSystemTreeModel.FLAG_DEFAULT
def parent(self, index):
node = self.getNode(index)
parent = node.parent
if parent == self.root:
return QModelIndex()
return self.createIndex(parent.row(), 0, parent)
def index(self, row, column, parent):
node = self.getNode(parent)
child = node.child(row)
if not child:
return QModelIndex()
return self.createIndex(row, column, child)
def headerData(self, section, orientation, role):
return self.root.name
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == Qt.DisplayRole:
return node.name
else:
return None
def insertNodes(self, position, nodes, parent=QModelIndex()):
node = self.getNode(parent)
self.beginInsertRows(parent, position, position + len(nodes) - 1)
for child in nodes:
success = node.insert_child(position, child)
self.endInsertRows()
return success
app = QApplication(sys.argv)
model = FileSystemTreeModel(Node('Filename'), path='c:/')
tree = QTreeView()
tree.setModel(model)
tree.show()
sys.exit(app.exec_())
Can I retrieve a Method via reflection, somehow combine it with a target object, and return it as something that looks like a function in Scala (i.e. you can call it using parenthesis)? The argument list is variable. It doesn't have to be a "first-class" function (I've updated the question), just a syntactic-looking function call, e.g. f(args).
My attempt so far looks something like this (which technically is pseudo-code, just to avoid cluttering up the post with additional definitions):
class method_ref(o: AnyRef, m: java.lang.reflect.Method) {
def apply(args: Any*): some_return_type = {
var oa: Array[Object] = args.toArray.map { _.asInstanceOf[Object] }
println("calling: " + m.toString + " with: " + oa.length)
m.invoke(o, oa: _*) match {
case x: some_return_type => x;
case u => throw new Exception("unknown result" + u);
}
}
}
With the above I was able to get past the compiler errors, but now I have a run-time exception:
Caused by: java.lang.IllegalArgumentException: argument type mismatch
The example usage is something like:
var f = ... some expression returning method_ref ...;
...
var y = f(x) // looks like a function, doesn't it?
UPDATE
Changing the args:Any* to args:AnyRef* actually fixed my run-time problem, so the above approach (with the fix) works fine for what I was trying to accomplish. I think I ran into a more general issue with varargs here.
Sure. Here's some code I wrote that add an interface to a function. It's not exactly what you want, but I think it can be adapted with few changes. The most difficult change is on invoke, where you'll need to change the invoked method by the one obtained through reflection. Also, you'll have to take care that the received method you are processing is apply. Also, instead of f, you'd use the target object. It should probably look something like this:
def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]) = method match {
case m if /* m is apply */ => target.getClass().getMethod("name", /* parameter type */).invoke(target, args: _*)
case _ => /* ??? */
}
Anyway, here's the code:
import java.lang.reflect.{Proxy, InvocationHandler, Method}
class Handler[T, R](f: Function1[T, R])(implicit fm: Manifest[Function1[T, R]]) extends InvocationHandler {
def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]) = method.invoke(f, args: _*)
def withInterface[I](implicit m: Manifest[I]) = {
require(m <:< manifest[Function1[T, R]] && m.erasure.isInterface)
Proxy.newProxyInstance(m.erasure.getClassLoader(), Array(m.erasure), this).asInstanceOf[I]
}
}
object Handler {
def apply[T, R](f: Function1[T, R])(implicit fm: Manifest[Function1[T, R]]) = new Handler(f)
}
And use it like this:
trait CostFunction extends Function1[String, Int]
Handler { x: String => x.length } withInterface manifest[CostFunction]
The use of "manifest" there helps with syntax. You could write it like this:
Handler({ x: String => x.length }).withInterface[CostFunction] // or
Handler((_: String).length).withInterface[CostFunction]
One could also drop the manifest and use classOf instead with a few changes.
If you're not looking for a generic invoke that takes the method name--but rather, you want to capture a particular method on a particular object--and you don't want to get too deeply into manifests and such, I think the following is a decent solution:
class MethodFunc[T <: AnyRef](o: Object, m: reflect.Method, tc: Class[T]) {
def apply(oa: Any*): T = {
val result = m.invoke(o, oa.map(_.asInstanceOf[AnyRef]): _*)
if (result.getClass == tc) result.asInstanceOf[T]
else throw new IllegalArgumentException("Unexpected result " + result)
}
}
Let's see it in action:
val s = "Hi there, friend"
val m = s.getClass.getMethods.find(m => {
m.getName == "substring" && m.getParameterTypes.length == 2
}).get
val mf = new MethodFunc(s,m,classOf[String])
scala> mf(3,8)
res10: String = there
The tricky part is getting the correct type for the return value. Here it's left up to you to supply it. For example,if you supply classOf[CharSequence] it will fail because it's not the right class. (Manifests are better for this, but you did ask for simple...though I think "simple to use" is generally better than "simple to code the functionality".)