DEV Community

Cover image for getIntsFromRangeString UDF for ColdFusion
James Moberg
James Moberg

Posted on • Edited on

getIntsFromRangeString UDF for ColdFusion

Useful for validating a human-entered list of integers.

I was working on a search interface for a client where public visitors would be searching for a range of different values regarding event years, event place, ago, # of competitions, hours of training etc. I initially searched Google for "range slider" and found many interactive widgets that I could've added to the interface. I didn't personally like the experience as each slider represented a different range and it seemed weird to see them next to each other as they didn't represent the same thing.

Another option, initially proposed by the client, was to add a start & end drop-downs. This would mean generating hundreds of hidden <option> values so the visitor could make their selection... and then require additional client-side logic to chain the selects together so that the max wouldn't be less than the min (and vice-versa). I didn't like this either as it requires a lot of mouse & mental interaction to select + keep in sync with each other. (I personally prefer typing, pasting or leveraging browser extensions to save+autofill whenever possible.)

I thought it would be best to allow the user to type anything that they wanted, limit which characters could be entered (0-9, ,, \s and -), sanitize values upon onchange and perform the logic server-side to parse the numbers that were provided to both generate a list of integers (for use in a SQL IN query) and display visually back to the visitor. I found formatListAsSeries UDF on CFLib (from 2010) that accepts a numeric list and generates a series for visual display... all I needed to do is figure out how to do the exact opposite AND enforce logical min/max rules.

Here's my attempt...

<cfset tests = [
    ["numberList": "0, 2021- , -1978, 1980-1984,1988, 1999, 8888, abc, 1920, 1E3", "minInt":1977, "maxInt":2022]
    ,["numberList": "2,1,6,,7,8,2, 2, 10, 1-3, 9.5", "minInt":1, "maxInt":10]
    ,["numberList": "0, 6, 4, 22, 100-200, 5,  😀  ", "minInt":0, "maxInt":10]
]>

<cfoutput>
    <h3>getIntsFromRangeString(numberList, <i>minInt</i>, <i>maxInt</i>)</h3>
    <cfloop array="#tests#" index="test">
        <cfset a = racerSearch.getIntsFromRangeString(argumentcollection=test)>
        <b>getIntsFromRangeString("#test.numberList#", <i>#test.minInt#</i>, <i>#test.maxInt#</i>)</b><br>
        <b>SortedValues:</b> <tt>#serializeJson(a.sortedValues)#</tt><br>
        <b>SeriesString:</b> <tt>"#a.seriesString#"</tt>
        <hr>
    </cfloop>
</cfoutput>
Enter fullscreen mode Exit fullscreen mode

This generates the following output:

getIntsFromRangeString("0, 2021- , -1978, 1980-1984,1988, 1999, 8888, abc, 1920, 1E3", 1977, 2022)
SortedValues: [1977,1978,1980,1981,1982,1983,1984,1988,1999,2021,2022]
SeriesString: "1977-1978, 1980-1984, 1988, 1999, 2021-2022"
------------------------------------
getIntsFromRangeString("2,1,6,,7,8,2, 2, 10, 1-3, 9.5", 1, 10)
SortedValues: [1,2,3,6,7,8,10]
SeriesString: "1-3, 6-8, 10"
--------------------------------
getIntsFromRangeString("0, 6, 4, 22, 100-200, 5, 😀 ", 0, 10)
SortedValues: [0,4,5,6]
SeriesString: "0, 4-6"
Enter fullscreen mode Exit fullscreen mode

Check it out and let me know if I forgot anything. Enjoy!

2022-09-20 Update: I've made some updates for performance as well as a SQL string generator.

CFML Source Code

https://gist.github.com/JamoCA/db753001d64a6fd4d46d2f591769c589

<!--- getIntsFromRangeString 2022-9-013
getIntsFromRangeString UDF for ColdFusion / CFML
Parses integers using min/max rules to generate sorted array of INTs and visual display summarizing string.
By James Moberg / SunStar Media https://www.sunstarmedia.com/
Blog: https://dev.to/gamesover/getintsfromrangestring-udf-for-coldfusion-n74
Gist: https://gist.github.com/JamoCA/db753001d64a6fd4d46d2f591769c589
Tweet: https://twitter.com/gamesover/status/1569843303084101633
2022-09-15 Add support to parse "INT+" syntax.
2022-09-15 Add support for LT, LTE, GT and GTE syntax / Use byte temp value when generating struct of unique INTs
2022-09-15 Remove default minInt/maxInt arguments. (Assuming a default maximum of 2147483647 = extremely poor performance.)
2022-09-16 Updated to use java array functions instead of struct when collecting, uniqueifying and sorting INTs
2022-09-20 Updated to return raw SQL BETWEEN OR clauses (MSSQL only support 2,100 params). Explicit cast to INT for better JSON serialization.
--->
<cfscript>
struct function getIntsFromRangeString(
required string numberList
,required numeric minInt
,required numeric maxInt
,string colName="colName"
) hint="I accept a string of range/list values and return a struct with array of sorted integers, series text and SQL" {
local.response = [
"sortedValues": []
,"seriesString": ""
,"sql": ""
];
local.integers = [];
local.s = listtoarray(javacast("string", arguments.numberList).replaceAll("[^\d,\-\+]", ""));
if (!arraylen(local.s)) return local.response;
// identify start/end values to series that start/end with a dash; set to 0 if out-of-limits
for(local.a=1; local.a lte arraylen(local.s); local.a+=1){
local.i = local.s[local.a];
// Support for LT, LTE, GT, GTE
if (left(local.i,1) is "<"){
local.num = local.i.replaceAll("[^\d]", "");
if (left(local.i,2) neq "<=") local.num -= 1;
local.i = "-" & local.num;
}
if (left(local.i,1) is ">"){
local.num = local.i.replaceAll("[^\d]", "");
if (left(local.i,2) neq ">=") local.num += 1;
local.i = local.num & "-";
}
if (left(local.i,1) is "-" && isvalid("integer", right(local.i, len(local.i)-1))){
if (arguments.minInt lte val(right(local.i, len(local.i)-1))){
local.s[local.a] = arguments.minInt & local.i;
} else {
local.s[local.a] = -1;
}
} else if (listfind("+,-", right(local.i,1)) && isvalid("integer", left(local.i, len(local.i)-1))){
if (arguments.maxInt gte val(local.i)){
local.s[local.a] = val(local.i) & "-" & arguments.maxInt;
} else {
local.s[local.a] = -1;
}
}
}
// generate distinct struct with valid integers between min/max values.
local.byte = javacast("byte",0);
for(local.i in local.s){
if (isvalid("integer", local.i) && local.i gte arguments.minInt && local.i lte arguments.maxInt){
arrayappend(local.integers, javacast("int", local.i)); // single integer
} else if (listlen(local.i,"-") is 2 && isvalid("integer", listfirst(local.i,"-")) && isvalid("integer", listlast(local.i,"-"))) {
local.num1 = (int(val(local.i)) lt arguments.minInt) ? arguments.minInt : int(val(local.i));
local.num2 = (int(val(listlast(local.i,"-"))) gte arguments.maxInt) ? arguments.maxInt : int(val(listrest(local.i,'-')));
if (local.num1 lte local.num2){
for(local.a=local.num1; local.a lte local.num2; local.a+=1){
arrayappend(local.integers, javacast("int", local.a));
}
}
}
}
// Uniqueify and sort array values
local.response.sortedValues = createobject("java", "java.util.ArrayList").init(
createobject("java", "java.util.HashSet").init(local.integers)
);
arraysort(local.response.sortedValues, "numeric");
// formatListAsSeries borrowed from https://cflib.org/udf/formatListAsSeries
local.useLastNum = false;
local.lastNum = "";
for (local.currNum in local.response.sortedValues) {
if ( len(local.lastNum) eq 0 ) {
local.response.seriesString = local.response.seriesString & local.currNum;
local.lastNum = local.currNum;
local.useLastNum = false;
} else if ( local.lastNum eq local.currNum ) {
// same; ignore
} else if ( local.lastNum + 1 neq local.currNum ) {
if ( local.useLastNum ) {
local.response.seriesString = local.response.seriesString & local.lastNum;
}
local.response.seriesString = local.response.seriesString & ", " & local.currNum;
local.lastNum = local.currNum;
local.useLastNum = false;
} else {
if ( !local.useLastNum ) {
local.response.seriesString = local.response.seriesString & "-";
}
local.lastNum = local.currNum;
local.useLastNum = true;
}
}
if ( local.useLastNum ) {
local.response.seriesString = local.response.seriesString & local.lastNum;
}
// Generate SQL from seriesString
if (len(trim(arguments.colName)) && len(local.response.seriesString)){
local.ranges = listtoarray(local.response.seriesString);
local.sql = [];
for (local.range in local.ranges){
local.range = trim(local.range);
if (find("-", local.range)){
arrayappend(local.sql, "(#arguments.colName# BETWEEN #val(local.range)# AND #val(listrest(local.range,'-'))#)");
} else {
arrayappend(local.sql, "(#arguments.colName# = #val(local.range)#)");
}
}
if (arraylen(local.sql)){
local.response.sql = "(#arraytolist(local.sql, ' OR ')#)";
}
}
return local.response;
}
</cfscript>
<cfset tests = [
["numberList": "0, 2021- , -1978, 1980-1984,1988, 1999, 8888, abc, 1920, 1E3", "minInt":1977, "maxInt":2022, "colName":"E.EventYear"]
,["numberList": "2,1,6,,7,8,2, 2, 10, 1-3, 9.5", "minInt":1, "maxInt":10, "colName":"R.RacesFlown"]
,["numberList": "0, 6, 4, 22, 100-200, 5, 😀 ", "minInt":1, "maxInt":10, "colName":""]
,["numberList": "5+, 1, 25", "minInt":1, "maxInt":10]
,["numberList": "<5, >8", "minInt":1, "maxInt":10, "colName":"Racers.TotalHrs"]
,["numberList": "<=5 ,8", "minInt":1, "maxInt":10, "colName":"TotalHrs"]
,["numberList": ">5, 1", "minInt":1, "maxInt":10, "colName":"R.TotalHrs"]
,["numberList": ">=5, 3", "minInt":1, "maxInt":10, "colName":"[R].[TotalHrs]"]
]>
<cfoutput>
<h3>getIntsFromRangeString(numberList, <i>minInt</i>, <i>maxInt</i>)</h3>
<cfloop array="#tests#" index="test">
<cfset a = getIntsFromRangeString(argumentcollection=test)>
<b>getIntsFromRangeString("#test.numberList#", <i>#test.minInt#</i>, <i>#test.maxInt#</i>)</b><br>
<b>SortedValues:</b> <tt>#serializeJson(a.sortedValues)#</tt><br>
<b>SeriesString:</b> <tt>"#a.seriesString#"</tt><br>
<b>SQL:</b> <tt><cfif len(a.sql)>"#a.sql#"<cfelse><i>empty</i></cfif></tt>
<hr>
</cfloop>
</cfoutput>

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay