Smarty: replace middle part of string - smarty

Hello I have a smarty variable which has values like (Number, Number, Number) and I would like to replace the middle part:
2, 3, 4, 5, 6 -> 2-6
1, 2, 3, 4 -> 1-4
7, 8, 9, 10, 11 -> 7-11
So basicaly I want to first and the last number to be combined by a "-" the rest should be removed. How can I achieve this in smarty?
Thanks in advance!

Smarty is a templating system. It's main purpose is to represent the data it receives from the PHP code (in variables) as HTML or any other text format.
The easiest way to do transformations like the one you need is to prepare them in the PHP code and assign them to variables.
On the other hand, such a transformation is not a business rule but a presentation detail; accordingly, it must be implemented in the view (Smarty) and not in the model (the PHP code).
The best solution for Smarty is to write a Smarty plugin (a modifier) that implements this transformation.
This is a simple example of how such a modifier could look like:
/**
* Smarty modifier plugin
*
* #param string|array $range input string or array
* #param string $separator the value to use as separator (e.g. '...')
* #return string
*/
function smarty_modifier_range($range, $separator = '-')
{
// If the input is a string (like "1,2,3,4"), convert it to an array
if (is_string($range)) {
$range = explode(',', $range);
} elseif (! is_array($range)) {
// If it's neither string, nor array then
// this modifier has no effect on it
return $range;
}
// A range must have at least two components
if (count($range) < 2) {
// It's not a range
return implode(',', $range);
}
// Use only the first and the last item
return reset($range).$separator.end($range);
}
In order to use it you have to tell Smarty where to find it. Create a directory for this plugin (maybe you will write more Smarty plugins) and put the function in a file named modifier.range.php in this directory.
Use the Smarty function addPluginsDir() during the initialization of your Smarty object to add this directory to the list of directories where it looks for plugins.
$smarty = new Smarty();
$smarty->addPluginsDir(array('... the path of the new director here'));
The modifier accepts arrays and strings that contain values separated by comma. It extracts from them the first and the last component (if there are at least two components) and return them joined by a configurable separator (default: -):
$smarty->assign('var1', range(12, 16));
$smarty->assign('var2', array(7, 8, 9, 10, 11));
$smarty->assign('var3', '2,3,4,5,6');
$smarty->assign('var4', '4');
$smarty->assign('var5', array(5, 10, 15));
$smarty->assign('var6', array(3, 5, 6, 4));
In templates:
{$var1|range} // 12-16 (modify array)
{$var2|range:".."} // 7..11 (use custom separator)
{$var3|range} // 2..5 (modify string)
{$var4|range} // 4 (there is no range here)
{$var5|range} // 5-15 (doesn't verify for gaps)
{$var6|range} // 3-4 (doesn't verify the order)
The last example shows a drawback of the plugin as it is exposed above. It doesn't verify if the values in the input array or string are sorted in the ascending order. Therefore, if they are not already sorted, its output is not quite correct.
However, this is a minor issue that can be easily solved using the sort PHP function.
Update: a more elaborate version of the plugin can be found on Github.

Are you sure that your variable is a string and not an array?
For an array: You could use foreach loop to process all array elements, but display only first and the last items.
Here is exactly what you need:
http://www.smarty.net/forums/viewtopic.php?p=3219
But for a string you could explode it on "," to get an array or regex_replace modifier:
http://www.smarty.net/docsv2/en/language.modifier.regex.replace.tpl

in java you can do this:
List<String> splitedList = Arrays.asList(variable.split(","));
String finalString=splitedList.get(0)+"-"+splitedList.get(splitedList.size()-1);

Related

Office Script - Split strings in vector & use dynamic cell address - Run an excel script from Power Automate

I'm completely new on Office Script (with only old experience on Python and C++) and I'm trying to run a rather "simple" Office Script on excel from power automate. The goal is to fill specific cells (always the same, their position shouldn't change) on the excel file.
The power Automate part is working, the problem is managing to use the information sent to Excel, in excel.
The script take three variables from Power automate (all three strings) and should fill specific cells based on these. CMQ_Name: string to use as is.
Version: string to use as is.
PT_Name: String with names separated by a ";". The goal is to split it in as much string as needed (I'm stocking them in an Array) and write each name in cells on top of each other, always starting on the same position (cell A2).
I'm able to use CMQ_Names & Version and put them in the cell they're supposed to go in, I've already make it works.
However, I cannot make the last part (in bold above, part 2 in the code below) work.
Learning on this has been pretty frustrating as some elements seems to sometime works and sometimes not. Newbie me is probably having syntax issues more than anyting...  
function main(workbook: ExcelScript.Workbook,
CMQ_Name: string,
Version: string,
PT_Name: string )
{
// create reference for each sheet in the excel document
let NAMES = workbook.getWorksheet("CMQ_NAMES");
let TERMS = workbook.getWorksheet("CMQ_TERMS");
//------Part 1: Update entries in sheet CMQ_NAMES
NAMES.getRange("A2").setValues(CMQ_Name);
NAMES.getRange("D2").setValues(Version);
//Update entries in sheet CMQ_TERMS
TERMS.getRange("A2").setValues(CMQ_Name);
//-------Part 2: work with PT_Name
//Split PT_Name
let ARRAY1: string[] = PT_Name.split(";");
let CELL: string;
let B: string = "B"
for (var i = 0; i < ARRAY1.length; i++) {
CELL = B.concat(i.toString());
NAMES.getRange(CELL).setValues(ARRAY1[i]);
}
}
  I have several problems:
Some parts (basically anything with red are detected as a problem and I have no idea why. Some research indicated it could be false positive, other not. It's not the biggest problem either as it seems the code sometimes works despite these warnings.
Argument of type 'string' is not assignable to parameter of type '(string | number | boolean)[ ][ ]'.
I couldn't find a way to use a variable as address to select a specific cell to write in, which is preventing the for loop at the end from working.  I've been bashing my head against this for a week now without solving it.
Could you kindly take a look?
Thank you!!
I tried several workarounds and other syntaxes without much success. Writing the first two strings in cells work, working with the third string doesn't.
EDIT: Thanks to the below comment, I managed to make it work:
function main(
workbook: ExcelScript.Workbook,
CMQ_Name: string,
Version: string,
PT_Name: string )
{
// create reference for each table
let NAMES = workbook.getWorksheet("CMQ_NAMES");
let TERMS = workbook.getWorksheet("CMQ_TERMS");
//------Part 0: clear previous info
TERMS.getRange("B2:B200").clear()
//------Part 1: Update entries in sheet CMQ_NAMES
NAMES.getRange("A2").setValue(CMQ_Name);
NAMES.getRange("D2").setValue(Version);
//Update entries in sheet CMQ_TERMS
TERMS.getRange("A2").setValue(CMQ_Name);
//-------Part 2: work with PT_Name
//Split PT_Name
let ARRAY1: string[] = PT_Name.split(";");
let CELL: string;
let B: string = "B"
for (var i = 2; i < ARRAY1.length + 2; i++) {
CELL = B.concat(i.toString());
//console.log(CELL); //debugging
TERMS.getRange(CELL).setValue(ARRAY1[i - 2]);
}
}
You're using setValues() (plural) which accepts a 2 dimensional array of values that contains the data for the given rows and columns.
You need to look at using setValue() instead as that takes a single argument of type any.
https://learn.microsoft.com/en-us/javascript/api/office-scripts/excelscript/excelscript.range?view=office-scripts#excelscript-excelscript-range-setvalue-member(1)
As for using a variable to retrieve a single cell (or set of cells for that matter), you really just need to use the getRange() method to do that, this is a basic example ...
function main(workbook: ExcelScript.Workbook) {
let cellAddress: string = "A4";
let range: ExcelScript.Range = workbook.getWorksheet("Data").getRange(cellAddress);
console.log(range.getAddress());
}
If you want to specify multiple ranges, just change cellAddress to something like this ... A4:C10
That method also accepts named ranges.

Matching & Comparing Strings via Levenshtein Distance

I have one table with a static list of "template" strings, and another sheet that updates daily with new "populated" strings.
I want to be able to identify which "template" each string is based on by finding the string with the lowest distance, and then displaying the distance between the template and the new string as a percentile.
Is it possible to do a Levenshtein distance in Excel without having to resort to Macros?
I found this function for calculating the distance, but I'm a novice with this kind of code, and I'm not sure how to include the step for locating the closest match before displaying the distance. It also seems like this might not work for long strings (200+ characters) like I'm dealing with.
I'm a sheets guy, not normally a code guy, and I'm up against a brick wall here in terms of my understanding of what to do next. Thank you for your help!
It is possible to calculate the Levenshtein distance between two text strings using a named function such as ztiaa's LEVDIST.
But it may be more practical to use a custom function like the one you link to, or the following implementation that fuzzy matches values to a corpus using fuzzyset.js which is a JavaScript port of the Python fuzzyset.
/**
* Finds the closest matches to words using the FuzzySet library.
*
* #param {A2:A} words_to_find The text strings to find matches for.
* #param {Dictionary!W2:W} dictionary A list of words to match words_to_find against.
* #param {0.5} match_score_min Optional. A threshold value for how good the match must be. 1 means an exact match. The default is 0.33.
* #return {Array} The closest match to words_to_find found in the dictionary, or a blank value if there is no match.
* #customfunction
*/
function FindFuzzyMatch(words_to_find, dictionary, match_score_min) {
'use strict';
// version 1.2, written by --Hyde, 14 September 2022
// - hat tip to Andreas Muller
// - uses fuzzyset.js by Glen Chiacchieri, a port of fuzzyset by Mike Axiak
if (arguments.length < 2 || arguments.length > 3) {
throw `Wrong number of arguments to FindFuzzyMatch. Expected 2 or 3 arguments, but got ${arguments.length} arguments.`;
}
const words = Array.isArray(words_to_find) ? words_to_find : [[words_to_find]];
const dict = Array.isArray(dictionary) ? dictionary.flat() : [dictionary];
const fuzzyDictionary = FuzzySet(dict);
return words.map(row => row.map(word => {
if (!word) {
return null;
}
const fuzzyMatches = fuzzyDictionary.get(word, null, match_score_min);
return fuzzyMatches ? fuzzyMatches[0][1] : null; // get just the first result, ignoring its score
}));
}
// include https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js below,
// and remove the final export {} block

Write a function index -- takes list and a number, and returns the index of the first element with the specified value

Wouldn't this question need 3 parameters instead of 2, being the list, index, and item being added?
You don't tell us what language you're working in but your function will have 2 parameters: the list and the value looked for, and 1 return type. If it is Java it might be:
public static int findIndex(String[] myList, String valueToLookFor) {
int indice;
// calculations here
return indice;
}
I believe in some languages you can speak if IN parameters and OUT parameters in which case yes, there are 2 IN parameters plus 1 OUT parameter = 3.

How do I sort Discord.Collection by keys?

I have a Discord.Collection of key-value pairs that stores the information about the number of commands in folders. It looks something like this:
debug - 2
utility - 2
fun - 3
best - 4
replies - 3
I want to sort the collection by folder names (keys) alphabetically (ascending). However, Discord.Collection.sort() sorts ascending by values, meaning my output is debug, utility, fun, replies, best instead of desired best, debug, fun, replies, utility output. Because Discord.Collection extends Map, I looked up js map documentation, but there is no .sort() method. I also looked up sorting in StackOverflow and google, but I only found answers regarding value sorting (or answer in different coding language, which I failed to translate).
I know .sort() accepts lambda expression as parameter, but I don't know how to use it for key sorting - I only ever used it for value sorting.
I don't think the code is necessary for this question, but in case you need to visualise my problem, here is creation of my collection:
const FolderCollection = new Discord.Collection();
//commandList is a Collection of key: command.name, value: {command: command, folder: folder}
commandList.each((cmd) => {
if (FolderCollection.has(cmd.folder)) {
FolderCollection.set(
cmd.folder,
FolderCollection.get(cmd.folder) + 1
);
} else {
FolderCollection.set(cmd.folder, 1);
}
});
Here I want to use my sorted FolderCollection:
FolderCollection.sort().each((cmdCount, categoryName) => {
//...do stuff
});
Feel free to ask for any additional details, but I believe this is all you need to help me with my issue (either direct answer, or link to documentation).
You could use Collection#entries() in a spread operator to turn the collection into an array with the structure [[key, value]], sort that because you'll have access to the whole array (key and value), and then convert it back into a collection if needed. Here's an example with a Map.
// let's say my objective is to sort these
// from lowest to highest relative to the key
// so the result should be:
// Map (3) { 1 => 'bar', 2 => 'baz', 3 => 'foo' }
const map = new Map([[3, 'foo'], [1, 'bar'], [2, 'baz']]);
// make use of [...iterable] to turn an iterable
// into an array
const entries = [...map.entries()];
console.log('Entries: ', entries);
// use array destructuring to access the
// first element of both arrays
entries.sort(([a], [b]) => a - b);
console.log('Sorted Entries: ', entries);
// convert it back to a map (or collection)
// if needed
const newMap = new Map(entries);

Is using >= and <= to specify enum values good code practice?

I am working on a project with about 8 other people, and would like to know the best code practice here, given that other people will be working on this code for years to come.
Say I have an enum with 10 values:
typedef enum {
Tag1 = 1,
Tag2,
Tag3,
Tag4,
Tag5,
Tag6,
Tag7,
Tag8,
Tag9,
Tag10
} Tag;
If I wanted to check if a tag is equal to Tag6, Tag7, Tag8, Tag9, or Tag10, is it good practice to using a comparison like:
if(myTag >= Tag6 && myTag <= Tag10) {
//Do something
}
?
Or is it best to use an OR and check for each tag?
Using >= and <= looks nicer and is less clunky, but if down the line, someone were to insert a new Tag between Tag7 and Tag8, it would mess up all the logic.
Can I expect that someone wouldn't add a new Tag between other Tags?
Yes, but only for enums that express a scale of values, for instance:
enum Priority {
None = 0,
Low,
Medium,
High,
Critical
}
Then this code makes sense and is readable:
if(message.Priority >= Priority.Medium) {
// Notify user
}
If the enum doesn't express a scale like this then avoid using < or > as they can be rather confusing. Use bit flags instead.
Flags enums use binary values so that values can be combined:
enum UserAudiences {
// Basic values: dec // binary
None = 0, // 0000
Client = 1, // 0001
Employee = 2, // 0010
Contractor = 4, // 0100
Key = 8, // 1000
// Combined: dec // binary
KeyClient = 9, // 1001 : Key + Client
BoardMember = 10, // 1010 : Key + Employee
CounterParty = 5, // 0101 : Client + Contractor
BusinessPartner = 13 // 1101 : Key + Client + Contractor
}
Then, when checking for a combined enum value we look at the binary number and whether the appropriate bit is set. For instance if we want to check for UserAudiences.Employee we can just look for the bit that represents 2, if it is set then we have one of the enum values that includes it:
if((message.Audience & UserAudiences.Employee) != 0) {
// Post on intranet
} else {
// Send externally
}
There's no way to set that bit through any combination of Key, Client or Contractor enums, it can only be set if Employee is one of the 'source' enums.
Most languages have helpers for this (or you can write your own):
if(message.Audience.HasFlag(UserAudiences.Employee)) { ...
The maths could work in any base - you could use 1, 10, 100, etc in decimal. However, you'd need much bigger numbers much sooner.
Finally, there's a convention to use singular names for regular enums, and plural names for flagged enums, hinting to the programmer whether to use equality or bitwise checks.
Can I expect that someone wouldn't add a new Tag between other Tags?
I wouldn't bet on it. Unless the enum's ordinal/underlying values have some inherent meaning or order I would avoid using them to much.
I would only use range checks if I actually want somebody to be able to insert additional enums without adapting all checks. This is probably a rather rare case though. Keith gives a good examples with the Priority enum, another example I can think of are log levels.
The exact syntax depends on the language of course but I would usually consider something like this as most readable:
if(myTag in [Tag6, Tag7, Tag8]) {
// ...
}
Or even better use some describing variable names which make it obvious what the other tags are:
topTags = [Tag6, Tag7, Tag8]
if(myTag in topTags) {
// ...
}

Resources