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>

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Image of Timescale

Timescale ā€“ the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

šŸ‘‹ Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay