Question

Hi there, today I have a question about the SchemaMapper lookup table. The SchemaMapper filter allows me to define conditional clauses to perform attribute mappings based on specific conditions.

  • 8 February 2022
  • 4 replies
  • 14 views

Badge

Let's assume we have a feature attribute called "two_digit_number". The attribute can have any integer value from 10 to 99. The condition "not an unlucky number" is met, if the value is not "13".

 

I was wondering if I could represent this condition in a single line of my SchemaMapper's lookup table? Something like two_digit_number != 13.

 

I could probably use a "TesterFilter" to filter out features with unlucky numbers before they enter the SchemaMapper - however, in my project I have dozens of attributes that need to be handled in a similar way. I would have to send all of them through a "TesterFilter". And this is what I would like to avoid.

 

I am excited to learn if there is a simple solution I may have overlooked.


4 replies

Userlevel 2
Badge +10

Hi @friedhelm​, we have an article on this topic that explores how we can make use of the Filter Features action in the SchemaMapper to perform conditional clauses. You can find the article here: https://community.safe.com/s/article/conditional-attribute-mapping-schemamapper

 

Hope this helps.

Badge +2

@friedhelm​ The real answer is no, you can't use function like two_digit_number != 13 as a filter in the SchemaMapper. It's something we are looking into supporting and I'd encourage you to add your vote to this Idea. Please add any additional comments around your use case.

 

But... one of our long time FME users has come up with a workaround, that in some cases might be useful. They tricked FME into executing an FME function in the schema mapping table. This cannot be accomplished on a filter, but can be done on an attribute value. 

Example: I have a dataset with "num_measures" and I want to map values 1 thru 10 to A thru J and any num_measure > 10 to Z.

The schema mapping table will look something like this:

FilterAttribute|FilterValue|TargetAttribute|TargetValue
num_measures|1|Type|A
num_measures|2|Type|B
num_measures|3|Type|C
...

But how to map num_measures > 10 = Z ?

We can add a row with an @EvaluateExpression() function:

FilterAttribute|FilterValue|TargetAttribute|TargetValue
num_measures||Type|@EvaluateExpression(<function>) 
num_measures|1|Type|A
num_measures|2|Type|B
num_measures|3|Type|C
...

In SchemaMapper, you put your least specific filters first, followed by the more specific filters. Using FilterValue = <null> will trigger the mapping for all input features and then if, say, num_measures = 2 this will subsequently get re-mapped to B.

In this case I'm going to use the math function if then else:

@Evaluate(@Value(num_measures) > 10 ? "Z" : @Value(num_measures))

So if num_measures > 10 set to Z else keep num_measures. Subsequent filters will handle values <= 10

How to format the function? There are two steps:

  • figure out the function you need - with say AttributeCreator or ExpressionEvaluator
  • convert the function to FME Parsable Text encoding
  • test the encoded it with FMEFunctionCaller
  • add the function to the schema table

OK, four steps plus some experimentation.

We have the function above. It must be executed in @EvaluateExpression() and will look like this:

@EvaluateExpression(UNUSED,@Evaluate(@Value(num_measures) > 10 ? "Z" : @Value(num_measures)),UNUSED)

Except, our function has to be in FME's internal parsable text encoding (similar to HTML text encoding). So we actually need this:

@EvaluateExpression(UNUSED,<at>Evaluate<openparen><openparen><at>Value<openparen>num_measures<closeparen><space><gt><space>10<closeparen><space>?<space><quote>Z<quote><space>:<at>Value<openparen>num_measures<closeparen><closeparen>,UNUSED2)

which is why I've used pipe (|) in my schema mapping table as the @EvaluateFunction() function includes commas.

 

It's a hassle creating FME Parsable Text so we have an FMEObjects function encodeToFMEParsableText to help do that and you can call that in the PythonCaller transformer. 

I've attached an example workspace (FME 2021.2) and schema mapping table. The workspace includes:

  • Original function in the AttributeCreator
  • FMEFunctionCaller to test the FME Parsable Text encoded function
  • SchemaMapper and schema mapping table with the encoded function
  • Example PythonCaller with the FMEParsableText encoding function

 

Badge

@friedhelm​ The real answer is no, you can't use function like two_digit_number != 13 as a filter in the SchemaMapper. It's something we are looking into supporting and I'd encourage you to add your vote to this Idea. Please add any additional comments around your use case.

 

But... one of our long time FME users has come up with a workaround, that in some cases might be useful. They tricked FME into executing an FME function in the schema mapping table. This cannot be accomplished on a filter, but can be done on an attribute value. 

Example: I have a dataset with "num_measures" and I want to map values 1 thru 10 to A thru J and any num_measure > 10 to Z.

The schema mapping table will look something like this:

FilterAttribute|FilterValue|TargetAttribute|TargetValue
num_measures|1|Type|A
num_measures|2|Type|B
num_measures|3|Type|C
...

But how to map num_measures > 10 = Z ?

We can add a row with an @EvaluateExpression() function:

FilterAttribute|FilterValue|TargetAttribute|TargetValue
num_measures||Type|@EvaluateExpression(<function>) 
num_measures|1|Type|A
num_measures|2|Type|B
num_measures|3|Type|C
...

In SchemaMapper, you put your least specific filters first, followed by the more specific filters. Using FilterValue = <null> will trigger the mapping for all input features and then if, say, num_measures = 2 this will subsequently get re-mapped to B.

In this case I'm going to use the math function if then else:

@Evaluate(@Value(num_measures) > 10 ? "Z" : @Value(num_measures))

So if num_measures > 10 set to Z else keep num_measures. Subsequent filters will handle values <= 10

How to format the function? There are two steps:

  • figure out the function you need - with say AttributeCreator or ExpressionEvaluator
  • convert the function to FME Parsable Text encoding
  • test the encoded it with FMEFunctionCaller
  • add the function to the schema table

OK, four steps plus some experimentation.

We have the function above. It must be executed in @EvaluateExpression() and will look like this:

@EvaluateExpression(UNUSED,@Evaluate(@Value(num_measures) > 10 ? "Z" : @Value(num_measures)),UNUSED)

Except, our function has to be in FME's internal parsable text encoding (similar to HTML text encoding). So we actually need this:

@EvaluateExpression(UNUSED,<at>Evaluate<openparen><openparen><at>Value<openparen>num_measures<closeparen><space><gt><space>10<closeparen><space>?<space><quote>Z<quote><space>:<at>Value<openparen>num_measures<closeparen><closeparen>,UNUSED2)

which is why I've used pipe (|) in my schema mapping table as the @EvaluateFunction() function includes commas.

 

It's a hassle creating FME Parsable Text so we have an FMEObjects function encodeToFMEParsableText to help do that and you can call that in the PythonCaller transformer. 

I've attached an example workspace (FME 2021.2) and schema mapping table. The workspace includes:

  • Original function in the AttributeCreator
  • FMEFunctionCaller to test the FME Parsable Text encoded function
  • SchemaMapper and schema mapping table with the encoded function
  • Example PythonCaller with the FMEParsableText encoding function

 

Thank you Mark. I will give it a try.
Friedhelm
Badge

@friedhelm​ The real answer is no, you can't use function like two_digit_number != 13 as a filter in the SchemaMapper. It's something we are looking into supporting and I'd encourage you to add your vote to this Idea. Please add any additional comments around your use case.

 

But... one of our long time FME users has come up with a workaround, that in some cases might be useful. They tricked FME into executing an FME function in the schema mapping table. This cannot be accomplished on a filter, but can be done on an attribute value. 

Example: I have a dataset with "num_measures" and I want to map values 1 thru 10 to A thru J and any num_measure > 10 to Z.

The schema mapping table will look something like this:

FilterAttribute|FilterValue|TargetAttribute|TargetValue
num_measures|1|Type|A
num_measures|2|Type|B
num_measures|3|Type|C
...

But how to map num_measures > 10 = Z ?

We can add a row with an @EvaluateExpression() function:

FilterAttribute|FilterValue|TargetAttribute|TargetValue
num_measures||Type|@EvaluateExpression(<function>) 
num_measures|1|Type|A
num_measures|2|Type|B
num_measures|3|Type|C
...

In SchemaMapper, you put your least specific filters first, followed by the more specific filters. Using FilterValue = <null> will trigger the mapping for all input features and then if, say, num_measures = 2 this will subsequently get re-mapped to B.

In this case I'm going to use the math function if then else:

@Evaluate(@Value(num_measures) > 10 ? "Z" : @Value(num_measures))

So if num_measures > 10 set to Z else keep num_measures. Subsequent filters will handle values <= 10

How to format the function? There are two steps:

  • figure out the function you need - with say AttributeCreator or ExpressionEvaluator
  • convert the function to FME Parsable Text encoding
  • test the encoded it with FMEFunctionCaller
  • add the function to the schema table

OK, four steps plus some experimentation.

We have the function above. It must be executed in @EvaluateExpression() and will look like this:

@EvaluateExpression(UNUSED,@Evaluate(@Value(num_measures) > 10 ? "Z" : @Value(num_measures)),UNUSED)

Except, our function has to be in FME's internal parsable text encoding (similar to HTML text encoding). So we actually need this:

@EvaluateExpression(UNUSED,<at>Evaluate<openparen><openparen><at>Value<openparen>num_measures<closeparen><space><gt><space>10<closeparen><space>?<space><quote>Z<quote><space>:<at>Value<openparen>num_measures<closeparen><closeparen>,UNUSED2)

which is why I've used pipe (|) in my schema mapping table as the @EvaluateFunction() function includes commas.

 

It's a hassle creating FME Parsable Text so we have an FMEObjects function encodeToFMEParsableText to help do that and you can call that in the PythonCaller transformer. 

I've attached an example workspace (FME 2021.2) and schema mapping table. The workspace includes:

  • Original function in the AttributeCreator
  • FMEFunctionCaller to test the FME Parsable Text encoded function
  • SchemaMapper and schema mapping table with the encoded function
  • Example PythonCaller with the FMEParsableText encoding function

 

Hi Mark,

 

Thanks to you (and Peter Laulund) for all this detail, it's been really useful towards a similar problem I've had with SchemaMapper.

Also a shoutout of thanks to Trent @ Safe who really helped with some gremlins around needing quotes on the attribute references. 

 

Sharing this here in case the solution can help others, as there are a couple of hazards that can trip things up. 

 

One situation that comes up a lot is we need to change data from millimetres to metres, or vice versa. Hard coded values can make a routine limited (though I built a semi-workaround using a single attribute that was actually a list of attributes to be changed and what the factor of change needed to be), so it would be great to do it within the SchemaMapper configuration.

 

It's relatively easy to do a calc if you know the value is going to be numeric, in this case taking the "attr_Size" value and dividing by 1000 to turn mm into m:

@Evaluate(@Value(attr_Size)/1000)

Except what if the "attr_Size" is something else ("-", "?"), or blank, null or missing? 

SchemaMapper throws an error and the routine aborts. 

 

How to get around this limitation?

Looking over the options in Arithmetic editor and Text editor, there is the Math Operator "?:" for If then else, as you used in above example. 

There is also the string function "@FindRegularExpression" that returns the position of the regex, or -1 if not found, and there are some well know Regex patterns as ways to test if a string is just a number.

As an If statement treats 0 as false, and anything else as true, the FindRegex can be used to test if the entire string is a number, and if so it will return 0, otherwise it returns -1. 

Looking at the "?:" if/then/else, the first then is actually the non-numeric return, second value is the numeric calculation to be returned. 

The following is what I built that will convert "attr_Size" from millimetres to metres if it is numeric, otherwise it returns the original value of "attr_Size" (such as blank, null, "-", etc). 

@Evaluate(@FindRegularExpression(@Value(attr_Size),^[+-]?(\d+(\.\d*)?|\.\d+)$)?"@Value(attr_Size)":"@Value(attr_Size)"/1000)

@FindRegularExpression(@Value(attr_Size),^[+-]?(\d+(\.\d*)?|\.\d+)$)      is the overall IF condition, should return either 0 or -1.

^[+-]?(\d+(\.\d*)?|\.\d+)$      is the regex pattern to test if "attr_Size" is numeric or not.

     the question mark just before the @Value is the IF of the overall evaluate, not to be confused with the "?" within the regex pattern.

"@Value(attr_Size)"      is the THEN return value of the IF; it's wrapped in double quote marks, without them it can cause inconsistent issues with some values.

:    then ELSE syntax, triggered by a false value in the test (in this case, 0), as index 0 occurs when the string is found to be numeric. 

"@Value(attr_Size)"/1000     is the ELSE return value, in this case the attr_Size will be numeric so a division by 1000 shouldnt cause an issue, and as per the THEN return value the quote marks are needed around the attribute.

 

Convert all of this expression out to the expanded parsable text, and wrap in an @EvaluateExpression, set return type to STRING, reference Transformer to UNUSED, and it should be good to go... 

@EvaluateExpression(STRING,{expanded text goes here},UNUSED)
 
eg:
 @EvaluateExpression(STRING,<at>Evaluate<openparen><at>FindRegularExpression<openparen><at>Value<openparen>attr_Size<closeparen><comma>^<openbracket>+-<closebracket>?<openparen><backslash>d+<openparen><backslash>.<backslash>d*<closeparen>?|<backslash>.<backslash>d+<closeparen><dollar><closeparen>?<quote><at>Value<openparen>attr_Size<closeparen><quote>:<quote><at>Value<openparen>attr_Size<closeparen><quote><solidus>1000<closeparen>,UNUSED)

 

Except there is another sting to come, around integer vs float division.

The Arithmetic editor assumes any division is float. 

However the EvaluateExpression will only assume float division if either the numerator or denominator is a float. 

If both num or denom are integer, then it assumes Integer division, which means any decimal numbers are truncated.

In my testing / experimentation, if the "attr_Size" was 600, divided by 1000, 600/1000 = 0.6. Except the value coming through in testing was 0 (truncating / removing the ".6")

The @EvaluateExpression documentation notes the "_FDIV" switch allows float division rather than integer division, but how to use it?

That documentation also lists basic syntax, and the second value shown has "DIV" as an extra leading parameter.

@EvaluateExpression(<retValType>, <FMEParsableText expr>, <transformername>)
@EvaluateExpression(DIV, <retValType>, <FMEParsableText expr>, <transformername>)

Unfortunately using "DIV" doesn't work or get the desired outcome. 

However, putting in "_FDIV" in that leading parameter does result in a floating division, the 0.6 from the earlier example.

@EvaluateExpression(_FDIV,STRING,<at>Evaluate<openparen><at>....

There is an alternative to using the "_FDIV" tag, and that is to perform a multiplication by the inverse of the divisor. ie, trying to convert mm to m, multiply by "*0.001" instead of divide "/1000". 

However this method is not always ideal as calculating the inverse may not be precise. 

eg 600 / 3 = 200, whereas 600 * 0.333 = 199.8.

 

-----

 

The potential of combining the if/then/else "?:" with a FindRegEx and wrapping attributes in quotes, opens up quite a few possibilities that may make it easier to encode in SchemaMapper config file, making the rest of the routine less reliant on hard coded values. 

 

More complex considerations could be setup with the right logic.

Consider if you had pipe / culvert features with a Size_Dia, Size_Width and Size_Height, and each feature should have a numeric value either in Size_Dia, or in both Size_Width and Size_Height. 

If it passes, then you want a new attribute of "Size_Valid" to be "True", otherwise "False".

The logic of this is:

(Size_Dia is a number || (Size_Width is a number && Size_Height is a number) ) ? "True" : "False"

(would need to setup the FindRegex == 0 for the "is a number" tests, rather than using my backwards logic in the earlier example).

 

This kind of conditional testing is not possible with the SchemaMapper filters, that can only test if the attribute has a specific value, or if the attribute is present on the feature regardless of value if the value field is left blank. 

 

Hopefully this info is of some value to someone...

 

Cheers

Luke T

----

Side notes: 

1) why bother doing this in SchemaMapper, rather than just within other transformers in the routine?

 

My company is looking at converting from and to many different schemas, so it is preferable to make one FME routine that has different schemamapper configuration files for each schema, than having to make a new FME routine for each input/output schema combo. 

It allows users to setup the configuration files in excel and use them through a routine FME server, so it reduces the amount of staff that need to know how to build or tweak the FME routine, they only have to know how to build the config file to handle new schemas

 

2) why bother doing this in SchemaMapper, why not just make a python script?

 

Two reasons. 

First, to sort this in python (or other scripting) would require building up some of the structure to have an input list of values to be changed - things already built in the schemamapper - and would be extra steps in the workflow. 

Second, where possible my company tries to avoid python scripting within FME. Some users are python-savvy but not all, and using python code makes it harder for some to make edits or changes to the routine; in contrast, the above process requires knowledge of regex and logic, but those will be expected of staff using FME. There may also be issues around maintaining python code as new versions of python and FME come out. 

Reply