From c1456fc750ee188815f25152da96acf6879776ca Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Wed, 2 Sep 2026 14:48:26 -0600 Subject: [PATCH 1/7] fix: allow blank values in domain constraint rule --- .../Roads_Edit/constrain_to_attribute_domain.arcade | 4 ++-- attribute-rules/test/constrain_to_attribute_domain.test.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade index 2faf2c3..d319241 100644 --- a/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade +++ b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade @@ -20,8 +20,8 @@ for (var i in fieldsToCheck) { var fieldName = fieldsToCheck[i]; var val = $feature[fieldName]; - // Skip empty/null values (let mandatory field settings handle null checks) - if (IsEmpty(val)) { continue; } + // Skip null and empty-string values while data cleanup is in progress. + if (IsEmpty(val) || val == '') { continue; } // Retrieve the domain assigned to this specific field var dom = Domain($feature, fieldName); diff --git a/attribute-rules/test/constrain_to_attribute_domain.test.ts b/attribute-rules/test/constrain_to_attribute_domain.test.ts index c050064..bb95c9a 100644 --- a/attribute-rules/test/constrain_to_attribute_domain.test.ts +++ b/attribute-rules/test/constrain_to_attribute_domain.test.ts @@ -34,6 +34,10 @@ test("constrain_to_attribute_domain accepts a coded domain value", () => { expect(executor.execute({ "$feature": roadFeature({ SPEED_LMT: 35 }) })).toBe(true); }); +test("constrain_to_attribute_domain accepts an empty string", () => { + expect(executor.execute({ "$feature": roadFeature({ SPEED_LMT: "" }) })).toBe(true); +}); + test("constrain_to_attribute_domain rejects an invalid coded domain value", () => { expect(executor.execute({ "$feature": roadFeature({ SPEED_LMT: 30 }) })).toEqual({ errorMessage: "Invalid value [30] entered for field: SPEED_LMT" From 24ef16f31ec94c62a18dc28e5aad8abf7453dc8d Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Wed, 2 Sep 2026 15:12:12 -0600 Subject: [PATCH 2/7] feat: uppercase --- .../Roads_Edit/fullname_calculation.arcade | 4 +- .../remove_apostrophes_from_name.arcade | 6 +-- .../calculation/Roads_Edit/unique_id.arcade | 8 ++-- .../test/remove_apostrophes_from_name.test.ts | 4 ++ .../test/text_calculations.test.ts | 38 ++++++++++--------- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade b/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade index 0bf3bf0..237740c 100644 --- a/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade +++ b/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade @@ -6,7 +6,7 @@ function normalizePart(value) { while (Find(" ", part) > -1) { part = Replace(part, " ", " "); } - return part; + return Upper(part); } var name = normalizePart(Replace(IIf(IsEmpty($feature.NAME), "", $feature.NAME), "'", "")); @@ -24,5 +24,5 @@ if (!IsEmpty(posttype)) { Push(parts, posttype); } -// Test case: NAME=" MAIN ", POSTDIR=null, POSTTYPE=" RD " -> "MAIN RD" +// Test case: NAME=" Main ", POSTDIR=null, POSTTYPE=" Rd " -> "MAIN RD" return Concatenate(parts, " "); \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade b/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade index 45fd3f0..7d526b2 100644 --- a/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade +++ b/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade @@ -1,7 +1,7 @@ /* global $feature, $datastore, $layer, $map, FeatureSetByName, IIf, Buffer */ // FIELD: NAME // EVENTS: INSERT, UPDATE -// Remove apostrophes from NAME field on CREATE and UPDATE events -// Test case: NAME="O'CONNELL" -> "OCONNELL" +// Remove apostrophes and normalize NAME to uppercase on CREATE and UPDATE events +// Test case: NAME="O'Connell" -> "OCONNELL" var name = IIf(IsEmpty($feature.NAME), "", Text($feature.NAME)); -return Replace(name, "'", ""); \ No newline at end of file +return Upper(Replace(name, "'", "")); \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/unique_id.arcade b/attribute-rules/calculation/Roads_Edit/unique_id.arcade index e7b08ab..a7e479e 100644 --- a/attribute-rules/calculation/Roads_Edit/unique_id.arcade +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -15,13 +15,13 @@ function trimCoordinate(value, leading) { // NAME/POSTDIR/POSTTYPE are computed directly because attribute rule execution order is not guaranteed, // so FULLNAME may still hold the pre-edit value. function nameSuffix() { - var name = Trim(IIf(IsEmpty($feature.NAME), '', Text($feature.NAME))); + var name = Upper(Trim(IIf(IsEmpty($feature.NAME), '', Text($feature.NAME)))); if (name == '') { return ''; } - var postdir = Trim(IIf(IsEmpty($feature.POSTDIR), '', Text($feature.POSTDIR))); - var posttype = Trim(IIf(IsEmpty($feature.POSTTYPE), '', Text($feature.POSTTYPE))); + var postdir = Upper(Trim(IIf(IsEmpty($feature.POSTDIR), '', Text($feature.POSTDIR)))); + var posttype = Upper(Trim(IIf(IsEmpty($feature.POSTTYPE), '', Text($feature.POSTTYPE)))); var value = '_' + name; if (posttype != '') { @@ -54,4 +54,4 @@ var grid1mil = gridMatch.GRID1MIL; var grid100k = gridMatch.GRID100K; // Test case: midpoint 12T VK 3756 1234 on "GOLD KEY LN" -> "12TVK37561234_GOLD_KEY_LN" -return Trim(grid1mil + grid100k + trimCoordinate(midpointGeom.x, 1) + trimCoordinate(midpointGeom.y, 2) + nameSuffix()); +return Upper(Trim(grid1mil + grid100k + trimCoordinate(midpointGeom.x, 1) + trimCoordinate(midpointGeom.y, 2) + nameSuffix())); diff --git a/attribute-rules/test/remove_apostrophes_from_name.test.ts b/attribute-rules/test/remove_apostrophes_from_name.test.ts index 52dc810..9887410 100644 --- a/attribute-rules/test/remove_apostrophes_from_name.test.ts +++ b/attribute-rules/test/remove_apostrophes_from_name.test.ts @@ -12,4 +12,8 @@ test("removes apostrophes from NAME", () => { test("leaves names without apostrophes unchanged", () => { expect(executor.execute({ "$feature": feature({ NAME: "WASHINGTON" }) })).toBe("WASHINGTON"); +}); + +test("uppercases mixed-case names", () => { + expect(executor.execute({ "$feature": feature({ NAME: "O'Connell" }) })).toBe("OCONNELL"); }); \ No newline at end of file diff --git a/attribute-rules/test/text_calculations.test.ts b/attribute-rules/test/text_calculations.test.ts index a849ed3..4e067f3 100644 --- a/attribute-rules/test/text_calculations.test.ts +++ b/attribute-rules/test/text_calculations.test.ts @@ -24,30 +24,34 @@ test("remove_extra_whitespace returns an empty string for a missing NAME", () => ).toBe(""); }); -test("fullname_calculation combines normalized name parts", () => { - expect( - fullnameExecutor.execute({ - $feature: feature({ - NAME: " O'MAIN ", - POSTDIR: null, - POSTTYPE: " RD ", - }), - }), - ).toBe("OMAIN RD"); +test("fullname_calculation combines normalized uppercase name parts", () => { + expect(fullnameExecutor.execute({ + "$feature": feature({ + NAME: " O'Main ", + POSTDIR: null, + POSTTYPE: " Rd " + }) + })).toBe("OMAIN RD"); }); test("fullname_calculation includes both suffixes for numeric names", () => { expect( fullnameExecutor.execute({ - $feature: feature({ NAME: "200", POSTDIR: "W", POSTTYPE: "St" }), + $feature: feature({ + NAME: "200", + POSTDIR: "W", + POSTTYPE: "St" + }), }), - ).toBe("200 W St"); + ).toBe("200 W ST"); }); test("fullname_calculation omits missing name parts", () => { - expect( - fullnameExecutor.execute({ - $feature: feature({ NAME: null, POSTDIR: " N ", POSTTYPE: null }), - }), - ).toBe("N"); + expect(fullnameExecutor.execute({ + "$feature": feature({ + NAME: null, + POSTDIR: " n ", + POSTTYPE: null + }) + })).toBe("N"); }); From 9e9822c76912a08408a8be48098ad73220bb781f Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Wed, 2 Sep 2026 16:58:19 -0600 Subject: [PATCH 3/7] feat: improved unique ID functionality --- .../calculation/Roads_Edit/unique_id.arcade | 32 ++++++++++++--- .../test/spatial_calculations.test.ts | 41 +++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/attribute-rules/calculation/Roads_Edit/unique_id.arcade b/attribute-rules/calculation/Roads_Edit/unique_id.arcade index a7e479e..b7aeff2 100644 --- a/attribute-rules/calculation/Roads_Edit/unique_id.arcade +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -1,4 +1,4 @@ -/* global $feature, $datastore, $layer, $map, FeatureSetByName, Intersects, First, Centroid, IIf, Buffer, Expects, Geometry, IsEmpty, Trim, Text, Count, Floor, Mid, Replace */ +/* global $feature, $datastore, $layer, $map, FeatureSetByName, Intersects, First, Centroid, IIf, Buffer, Expects, Geometry, IsEmpty, Trim, Text, Count, Floor, Mid, Replace, Find, Upper */ // Ports the legacy UtransEditor getNewUniqueID logic, reading GRID1MIL/GRID100K from the USNG. // Grid-cell zone mismatches at the UTM 11/12 boundary are ignored since this ID is only eyeballed, not parsed. // FIELD: UNIQUE_ID @@ -12,6 +12,17 @@ function trimCoordinate(value, leading) { return Mid(digits, leading, Count(digits) - leading - 1); } +function isLettersOnly(value) { + var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + for (var index = 0; index < Count(value); index++) { + if (Find(Mid(value, index, 1), letters) == -1) { + return false; + } + } + + return true; +} + // NAME/POSTDIR/POSTTYPE are computed directly because attribute rule execution order is not guaranteed, // so FULLNAME may still hold the pre-edit value. function nameSuffix() { @@ -24,11 +35,20 @@ function nameSuffix() { var posttype = Upper(Trim(IIf(IsEmpty($feature.POSTTYPE), '', Text($feature.POSTTYPE)))); var value = '_' + name; - if (posttype != '') { - value += '_' + posttype; - } - if (postdir != '') { - value += '_' + postdir; + if (isLettersOnly(name)) { + if (posttype != '') { + value += '_' + posttype; + } + if (postdir != '') { + value += '_' + postdir; + } + } else { + if (postdir != '') { + value += '_' + postdir; + } + if (posttype != '') { + value += '_' + posttype; + } } return Replace(value, ' ', '_'); diff --git a/attribute-rules/test/spatial_calculations.test.ts b/attribute-rules/test/spatial_calculations.test.ts index 1aee264..44b55e9 100644 --- a/attribute-rules/test/spatial_calculations.test.ts +++ b/attribute-rules/test/spatial_calculations.test.ts @@ -19,6 +19,31 @@ function emptyDatastore() { ] }); } +const uniqueIdLineGeometry = { + type: "polyline", + paths: [[[424100, 4505000], [432500, 4505000]]], + spatialReference: { wkid: 102100 } +}; + +function nationalGridDatastore() { + return new ArcGISMap({ layers: [ + new FeatureLayer({ + title: "NationalGrid", geometryType: "polygon", spatialReference: { wkid: 102100 }, + objectIdField: "OBJECTID", + fields: [ + { name: "OBJECTID", type: "oid" }, + { name: "GRID1MIL", type: "string" }, + { name: "GRID100K", type: "string" } + ], + source: [feature({ OBJECTID: 1, GRID1MIL: "12T", GRID100K: "VK" }, { + type: "polygon", + rings: [[[420000, 4500000], [435000, 4500000], [435000, 4510000], [420000, 4510000], [420000, 4500000]]], + spatialReference: { wkid: 102100 } + })] + }) + ] }); +} + const spatialExecutor = await loadExecutor("calculation/Roads_Edit/spatial_assignments.arcade", [ { name: "$feature", type: "feature" }, { name: "$datastore", type: "featureSetCollection" } ]); @@ -42,4 +67,20 @@ test("unique_id reports missing geometry", async () => { "$feature": feature({ NAME: "MAIN", POSTDIR: null, POSTTYPE: null }), "$datastore": emptyDatastore() }); expect(result).toBe("ERR:MISSING_GEOMETRY"); +}); + +test("unique_id appends post direction before post type for non-letter names", async () => { + const result = await uniqueIdExecutor.executeAsync({ + "$feature": feature({ NAME: "4500", POSTDIR: "W", POSTTYPE: "TER" }, uniqueIdLineGeometry), + "$datastore": nationalGridDatastore() + }); + expect(result).toBe("12TVK28300500_4500_W_TER"); +}); + +test("unique_id appends post type before post direction for letters-only names", async () => { + const result = await uniqueIdExecutor.executeAsync({ + "$feature": feature({ NAME: "Main", POSTDIR: "W", POSTTYPE: "TER" }, uniqueIdLineGeometry), + "$datastore": nationalGridDatastore() + }); + expect(result).toBe("12TVK28300500_MAIN_TER_W"); }); \ No newline at end of file From 8a0397051b0b33a410cd953ab4c6df843ef788da Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Thu, 3 Sep 2026 11:23:45 -0600 Subject: [PATCH 4/7] refactor: rename to be more accurate --- .../{remove_apostrophes_from_name.arcade => format_name.arcade} | 0 ...remove_apostrophes_from_name.test.ts => format_name.test.ts} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename attribute-rules/calculation/Roads_Edit/{remove_apostrophes_from_name.arcade => format_name.arcade} (100%) rename attribute-rules/test/{remove_apostrophes_from_name.test.ts => format_name.test.ts} (90%) diff --git a/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade b/attribute-rules/calculation/Roads_Edit/format_name.arcade similarity index 100% rename from attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade rename to attribute-rules/calculation/Roads_Edit/format_name.arcade diff --git a/attribute-rules/test/remove_apostrophes_from_name.test.ts b/attribute-rules/test/format_name.test.ts similarity index 90% rename from attribute-rules/test/remove_apostrophes_from_name.test.ts rename to attribute-rules/test/format_name.test.ts index 9887410..e8e205b 100644 --- a/attribute-rules/test/remove_apostrophes_from_name.test.ts +++ b/attribute-rules/test/format_name.test.ts @@ -2,7 +2,7 @@ import { expect, test } from "vitest"; import { feature, loadExecutor } from "./_helpers.js"; const executor = await loadExecutor( - "calculation/Roads_Edit/remove_apostrophes_from_name.arcade", + "calculation/Roads_Edit/format_name.arcade", [{ name: "$feature", type: "feature" }] ); From e5cac66e70bd402e939ed42d27176004b197ba26 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Thu, 3 Sep 2026 14:09:37 -0600 Subject: [PATCH 5/7] fix: Update isLettersOnly function to normalize input Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- attribute-rules/calculation/Roads_Edit/unique_id.arcade | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/attribute-rules/calculation/Roads_Edit/unique_id.arcade b/attribute-rules/calculation/Roads_Edit/unique_id.arcade index b7aeff2..20797c2 100644 --- a/attribute-rules/calculation/Roads_Edit/unique_id.arcade +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -14,8 +14,9 @@ function trimCoordinate(value, leading) { function isLettersOnly(value) { var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - for (var index = 0; index < Count(value); index++) { - if (Find(Mid(value, index, 1), letters) == -1) { + var normalized = Replace(value, ' ', ''); + for (var index = 0; index < Count(normalized); index++) { + if (Find(Mid(normalized, index, 1), letters) == -1) { return false; } } From d438ffc16867e580e5172564081ace5f88fd746c Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Thu, 3 Sep 2026 14:10:59 -0600 Subject: [PATCH 6/7] fix: Fix nameSuffix function to handle single quotes Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- attribute-rules/calculation/Roads_Edit/unique_id.arcade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attribute-rules/calculation/Roads_Edit/unique_id.arcade b/attribute-rules/calculation/Roads_Edit/unique_id.arcade index 20797c2..56f84a0 100644 --- a/attribute-rules/calculation/Roads_Edit/unique_id.arcade +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -27,7 +27,7 @@ function isLettersOnly(value) { // NAME/POSTDIR/POSTTYPE are computed directly because attribute rule execution order is not guaranteed, // so FULLNAME may still hold the pre-edit value. function nameSuffix() { - var name = Upper(Trim(IIf(IsEmpty($feature.NAME), '', Text($feature.NAME)))); + var name = Upper(Trim(Replace(IIf(IsEmpty($feature.NAME), '', Text($feature.NAME)), "'", ''))); if (name == '') { return ''; } From b9209765b5e5a860679cf7c5a5fdd4e533aea52f Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Thu, 3 Sep 2026 15:00:00 -0600 Subject: [PATCH 7/7] refactor: remove unneeded comment --- .../constraint/Roads_Edit/constrain_to_attribute_domain.arcade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade index d319241..0cbfcd4 100644 --- a/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade +++ b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade @@ -20,7 +20,7 @@ for (var i in fieldsToCheck) { var fieldName = fieldsToCheck[i]; var val = $feature[fieldName]; - // Skip null and empty-string values while data cleanup is in progress. + // Skip null and empty-string values if (IsEmpty(val) || val == '') { continue; } // Retrieve the domain assigned to this specific field