FileMaker Dictionary

Elsewhere on the intertubes there have been a few techniques for implementing dictionaries in FileMaker. If you aren't familiar with them, dictionaries (known in some languages as associative arrays or maps) allow the storage of key/value pairs. I've come across them in PHP, REALbasic and Objective-C, and they're quite useful. The basic idea is that you store key-value pairs of data. Each key can appear only once but can hold any kind of data.

Although I've used other people's dictionary techniques in my solutions, I've been unhappy with them for a number of reasons. First of all, I find them poorly documented. Even though some of them include lengthy articles regarding their use, wading through the articles is difficult and doesn't seem to bring much clarity. Second, I'm unhappy with their storage methods. Both of the solutions I've come across use esoteric delimiters for the data, making them difficult to read. Finally, they don't fully implement the dictionary ideal, in that keys can be duplicated. While this comes in handy because the one I was using in a recent solution didn't support lists as a data type, I prefer the cleanliness of single key restrictions.

So as much as I would prefer to use someone else's prewritten solution to this technique, I'm going to write my own.

Storage Format

The first task I see is to decide how to store the dictionary. With FileMaker we only have text as an option, and there are at least a couple of options for storing a dictionary as text. The two most obvious are XML and JSON.

Comparing the two, let's use as an example a dictionary with three entries: First Name, Last Name and Email Addresses. Email Addresses itself will store a dictionary with two entries, Work and Home. XML might store this data as follows:

<contact>
  <first_name>Charles</first_name>
  <last_name>Ross</last_name>
  <email_addresses>
    <work>This email address is being protected from spambots. You need JavaScript enabled to view it.</work>
    <home>This email address is being protected from spambots. You need JavaScript enabled to view it.</home>
  </email_addresses>
</contact>

The spacing is optional, but it does help the readability.

The same data expressed in JSON might look like this:

{
  "first_name":"Charles",
  "last_name":"Ross",
  "email_addresses":
  {
    "work":"This email address is being protected from spambots. You need JavaScript enabled to view it.",
    "home":"This email address is being protected from spambots. You need JavaScript enabled to view it."
  }
}

Note that both of these are "pretty printed," with spacing and returns. Removing all of both would still result in valid XML and JSON.

It seems to me that the XML format would be easier to create, but the JSON format is easier to read, so I've chosen JSON. I (hopefully) only need to write the solution once, but I'll be reading it hundreds of times over the years.

According to Wikipedia, JSON supports the storage of numbers, strings, boolean values, arrays, objects and null. Objects, in this context, seems to simply be dictionaries. In other words, a value in a JSON dictionary can be a JSON dictionary, which is what I demonstrated above with the email addresses. Dictionaries are indicated with curly braces, strings with double quotes and arrays with square brackets. Since FileMaker doesn't have a built-in boolean type (using 0 or an empty string to mean false with everything else interpreted as true), I'm going to forego the boolean type, since that can be solved with the number type. For the time being I'm not going to worry about null values, but plan to add that in the future.

Here's an example with all of the possible data types.

{
  "string":"a string of characters",
  "number":3.14,
  "dictionary":
  {
    "key1":"value1",
    "key2":"value2"
  },
  "array":
  [
    "value1",
    "value2"
  ]
}

Development Environment

I had originally planned to write the dictionary functions as custom functions within FileMaker, and had actually begun to do so. But the recursive text parsing was very difficult, to the point where I kept saying to myself that there must be an easier way. And when you're programming and say that to yourself, you're probably right.

First I thought of SmartPill PHP. I've programmed PHP for going on 8 years, so implementing this with PHP would be a pretty siimple matter, especially with PHP's support for associative arrays. But the downside to using it would be that anyone else wanting to integrate this would need that (not free) plugin.

But then I remember ScriptMaster. ScriptMaster is very similar to SmartPill PHP, but instead of executing PHP code, it executes Groovy code. Except for a couple of cases, I haven't gone much beyond the modules included with the plugin (I recently edited the included Post Data to URL module for a client), and I don't know Java (upon which Groovy is based), but figured I could learn enough to get the job done fairely quickly. More quickly, anyway, that writing FileMaker custom functions to parse the strings.

Groovy includes built-in JSON support, so it was just a matter of becoming familiar with Groovy's Maps and Lists (dictionaries and arrays), which took me an hour or so. Groovy's JSON support even prints out the dictionary with spacing, although it's not exactly was I would. It places the opening brace or bracket for a sub-dictionary or sub-array on the same line as the key, wereas I was placing it on the next line, a minor difference I can live with.

I've created a git repository to host the functions, or you can use a direct download, and really would welcome feedback. I'm new to Groovy, fairly new to publishing open source software, new to creating custom ScriptMaster modules, so ideas on best practices and possible additions are very welcome. I plan to refactor the code a bit already, as there's some code duplication, but I'll have to learn about Groovy functions or classes first.

Conventions

I wrote an article about custom function conventions that I anticipated would be relevant to this project, but as I'm not using custom functions for this, it isn't as relevant as I thought. Some still apply, but others either can't be used or aren't relevant to ScriptMaster modules.

As far as they're still relevant, I'll keep to them, but one point of difference is the four-character namespace convention. My custom function all begin with a four-character code followed by a dot and then the function name. The four-character code indicates which group of custom functions a particular function belongs to. The difference here will be that instead of a dot, an underscore will follow the namespace. ScriptMaster gives me an error when I try to register a function with a dot in the name.

Parameter names will still begin with an underscore, but variable names within the Groovy code will not.

Groovy Code

I'm going to list all of the Groovy code first and then I'll place that code in a FileMaker custom function that registers that code as plugin funcitons. Generally the process for each of these is as follows:

  1. Convert the array or dictionary into a native Groovy object.
  2. Perform the required operation.
  3. Convert the new array or dictionary into a JSON string and return it.

Array Functions

arry_Add( _array; _value )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_array = ( _array == null ) ? '[]' : _array

def slurper = new JsonSlurper()
array = slurper.parseText( _array )

if ( _value.isInteger() ) {
  _value = _value.toInteger()
} else if ( _value.isDouble() ) {
  _value = _value.toDouble()
} else if ( _value.startsWith( '[' )
          | _value.startsWith( '{' ) ) {
  _value = slurper.parseText( _value )
}

array.add(_value)

def json = new JsonBuilder(array)

return json.toPrettyString()

arry_Count( _array )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_array = ( _array == null ) ? '[]' : _array

def slurper = new JsonSlurper()
array = slurper.parseText( _array )

return array.size

arry_Value( _array; _index )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_array = ( _array == null ) ? '[]' : _array

def slurper = new JsonSlurper()
array = slurper.parseText( _array )

return array[_index.toInteger()]

arry_Head( _array )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_array = ( _array == null ) ? '[]' : _array

def slurper = new JsonSlurper()
array = slurper.parseText( _array )

return array.head()

arry_Tail( _array )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_array = ( _array == null ) ? '[]' : _array

def slurper = new JsonSlurper()
array = slurper.parseText( _array )

array = array.tail()

def json = new JsonBuilder( array )

return json.toPrettyString()

arry_IsEmpty( _array )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_array = ( _array == null ) ? '[]' : _array

def slurper = new JsonSlurper()
array = slurper.parseText( _array )

return array.size == 0

Dictionary Functions

dict_SetValueForKey( _dict; _key; _value )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_dict = ( _dict == null ) ? '{}' : _dict

def slurper = new JsonSlurper()
dict = slurper.parseText( _dict )

if ( _value.isInteger() ) {
  _value = _value.toInteger()
} else if ( _value.isDouble() ) {
  _value = _value.toDouble()
} else if ( _value.startsWith( '[' )
          | _value.startsWith( '{' ) ) {
  _value = slurper.parseText( _value )
}

dict[ _key ] = _value

def json = new JsonBuilder( dict )

return json.toPrettyString()

dict_GetValueForKey( _dict; _key )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_dict = ( _dict == null ) ? '{}' : _dict

def slurper = new JsonSlurper()
dict = slurper.parseText( _dict )

def value = dict[ _key ]

if ( ( value instanceof java.util.Map )
   | ( value instanceof java.util.List ) ) {
  def json = new JsonBuilder( value )
  value = json.toPrettyString()
}

return value

dict_RemoveKey( _dict; _key )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_dict = ( _dict == null ) ? '{}' : _dict

def slurper = new JsonSlurper()
dict = slurper.parseText( _dict )

dict.remove( _key )

def json = new JsonBuilder( dict )

return json.toPrettyString()

dict_IsEmpty( _dict )

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

_dict = ( _dict == null ) ? '{}' : _dict

def slurper = new JsonSlurper()
dict = slurper.parseText( _dict )

return dict.count{true} == 0

FileMaker Custom Function

Now that we have all of the Groovy code, we need to create calls to ScriptMaster's RegisterGroovy function for each of them. I've decided to use a single custom function to do this. I don't know if this is a good practice or not. I'm also using FileMaker calculation variables to substitute common code, again, unsure if this is the best way to go about it or not.

Regardless, here's the full calculation.

Let(
  [
    _code_prefix =
      "import groovy.json.JsonBuilder¶" &
      "import groovy.json.JsonSlurper¶" &
      "¶";
    
    _test_value_code =
      "if ( _value.isInteger() ) {¶" &
      "  _value = _value.toInteger()¶" &
      "} else if ( _value.isDouble() ) {¶" &
      "  _value = _value.toDouble()¶" &
      "} else if ( _value.startsWith( '[' ) ¶" &
      "          | _value.startsWith( '{' ) ) {¶" &
      "  _value = slurper.parseText( _value )¶" &
      "}¶" &
      "¶";
    
    _empty_array_code =
      "_array = ( _array == null ) ? '[]' : _array¶" &
      "¶";
    
    _empty_dict_code =
      "_dict = ( _dict == null ) ? '{}' : _dict¶" &
      "¶";
    
    _slurp_array_code =
      "def slurper = new JsonSlurper()¶" &
      "array = slurper.parseText( _array )¶" &
      "¶";
    
    _slurp_dict_code =
      "def slurper = new JsonSlurper()¶" &
      "dict = slurper.parseText( _dict )¶" &
      "¶";

    //---------------------------------------------------------------

    _arry_Add_template = "arry_Add( _array ; _value )";
    _arry_Add_code =
      _code_prefix &
      _empty_array_code &
      _slurp_array_code &
      _test_value_code &
      "array.add(_value)¶" &
      "¶" &
      "def json = new JsonBuilder(array)¶" &
      "¶" &
      "return json.toPrettyString()";

    _ = RegisterGroovy( _arry_Add_template; _arry_Add_code );

    //---------------------------------------------------------------

    _arry_Count_template = "arry_Count( _array )";
    _arry_Count_code =
      _code_prefix &
      _empty_array_code &
      _slurp_array_code &
      "return array.size";

    _ = RegisterGroovy( _arry_Count_template; _arry_Count_code );

    //---------------------------------------------------------------

    _arry_Value_template = "arry_Value( _array ; _index )";
    _arry_Value_code =
      _code_prefix &
      _empty_array_code &
      _slurp_array_code &
      "return array[ _index.toInteger() ]";

    _ = RegisterGroovy( _arry_Value_template; _arry_Value_code );

    //---------------------------------------------------------------

    _arry_Head_template = "arry_Head( _array )";
    _arry_Head_code =
      _code_prefix &
      _empty_array_code &
      _slurp_array_code &
      "return array.head()";

    _ = RegisterGroovy( _arry_Head_template; _arry_Head_code );

    //---------------------------------------------------------------

    _arry_Tail_template = "arry_Tail( _array )";
    _arry_Tail_code =
      _code_prefix &
      _empty_array_code &
      _slurp_array_code &
      "return array.tail()";

    _ = RegisterGroovy( _arry_Tail_template; _arry_Tail_code );

    //---------------------------------------------------------------

    _arry_IsEmpty_template = "arry_IsEmpty( _array )";
    _arry_IsEmpty_code =
      _code_prefix &
      _empty_array_code &
      _slurp_array_code &
      "return array.size == 0";

    _ = RegisterGroovy( _arry_IsEmpty_template; _arry_IsEmpty_code );

    //---------------------------------------------------------------

    _dict_SetValueForKey_template = "dict_SetValueForKey( _dict; _key; _value )";
    _dict_SetValueForKey_code =
      _code_prefix &
      _empty_dict_code &
      _slurp_dict_code &
      _test_value_code &
      "dict[ _key ] = _value¶" &
      "¶" &
      "def json = new JsonBuilder( dict )¶" &
      "¶" &
      "return json.toPrettyString()";

    _ = RegisterGroovy( _dict_SetValueForKey_template; _dict_SetValueForKey_code );

    //---------------------------------------------------------------

    _dict_GetValueForKey_template = "dict_GetValueForKey( _dict; _key )";
    _dict_GetValueForKey_code =
      _code_prefix &
      _empty_dict_code &
      _slurp_dict_code &
      "def value = dict[ _key ]¶" &
      "¶" &
      "if ( ( value instanceof java.util.Map )¶" &
      "   | ( value instanceof java.util.List ) ) {¶" &
      "  def json = new JsonBuilder( value )¶" &
      "  value = json.toPrettyString()¶" &
      "}¶" &
      "¶" &
      "return value";

    _ = RegisterGroovy( _dict_GetValueForKey_template; _dict_GetValueForKey_code );

    //---------------------------------------------------------------

    _dict_RemoveKey_template = "dict_RemoveKey( _dict; _key )";
    _dict_RemoveKey_code =
      _code_prefix &
      _empty_dict_code &
      _slurp_dict_code &
      "dict.remove( _key )¶" &
      "¶" &
      "def json = new JsonBuilder( dict )¶" &
      "¶" &
      "return json.toPrettyString()";

    _ = RegisterGroovy( _dict_RemoveKey_template; _dict_RemoveKey_code );

    //---------------------------------------------------------------

    _dict_IsEmpty_template = "dict_IsEmpty( _dict )";
    _dict_IsEmpty_code =
      _code_prefix &
      _empty_dict_code &
      _slurp_dict_code &
      "return dict.size == 0";

    _ = RegisterGroovy( _dict_IsEmpty_template; _dict_IsEmpty_code )

    //---------------------------------------------------------------

  ];

  ""
)

I place this in a custom function called RegisterFunctions. All that's then required to have access to all of those plugin functions is to execute RegisterFunctions. Also in the sample file is a testing custom function that should return 1 if everything is working correctly.


There's a lot more I can do with this, adding more capabilities for manipulating arrays and dictionaries, determining their equality, etc. But these form what I think is the basics.

Let me know what you think. Any and all feedback is welcome.

Comments  

 
0 # Tomas 2013-10-25 03:26
Thanks, these are very useful functions.
Reply | Reply with quote | Quote
 

Add comment


Security code
Refresh

Search

Products