The Rosetta Stone Project
When we work in PHP, or Python, or Ruby, or C#, we have certain tools. When we work in MultiValue, we have certain tools. I'm greedy, I want the best of both. This is part one in a series on how to create MultiValue features in PHP, Python, Ruby, Node.js, and C#.
Not only does this give us a taste of MV in the other languages we pick up, but it also becomes a sort of Rosetta Stone. For those who don't know the term, the Rosetta Stone was a tablet with three written languages on it, each declaring the same information. Having them side-by-side meant that if you could read any one of the language, you could use it as a guide toward learning the other two.
So, seeing code in C# or Ruby means that for people who know one of them, the parallels can help programmers bridge from one into the other. Hat tip to my cousin Alana for suggesting the Rosetta Stone metaphor.
To get this started, we've elected to implement some of the nicer features of OCONV.
DATE
We won't be implementing every variation of the Date Conversion but we will implement the core features. And, since you'll have the source code, so you can expand it. We'll get the "/" vs. "-" working [Figure 1].
PHP Code (Full code: https://github.com/CharlesBarouch/mv_core) <?php // mv_core.php // by Charles Barouch (Results@HDWP.com) // on 09/15/19 // Originally published in Intl-Spectrum.com // ----------------------------------------- function mv_oconv($value,$rule) { $rule_1 = strtoupper(substr($rule,0,1)); $rule_2 = substr($rule,0,2); if( $rule_1 == 'D') { $result = mv_oconv_date($value,$rule);} else if($rule_1 == 'G') { $result = mv_oconv_group($value,$rule);} else if($rule_2 == 'MT') { $result = mv_oconv_time($value,$rule);} return $result; } function mv_oconv_date($value,$rule) { $dt = new DateTime("1967-12-31 00:00:00"); $dt->Modify('+'.$value.' day'); $mdy = [$dt->format("m"),$dt->format("d"),$dt->format("Y")]; $dlmtr = '/'; if(strpos($rule,'-',1)) {$dlmtr = '-';} if(is_numeric(substr($rule,1,1))) { $mdy[2] = substr($mdy[2],-1*substr($rule,1,1)); } if(strpos($rule,'Y')) {$result = $mdy[2];} else { if(substr($rule,0,2) == 'DM') {$result = $mdy[0]; } else { if(substr($rule,0,2) == 'DD') {$result = $mdy[1]; } else { $result = $mdy[0] . $dlmtr . $mdy[1] . $dlmtr . $mdy[2]; } } } return $result; } ?> PYTHON (Full code: https://github.com/CharlesBarouch/mv_core) import datetime # mv_core.py # by Charles Barouch # on 09/15/19 # Originally published in Intl-Spectrum.com # ----------------------------------------- def oconv_date(value, rule): baseline = datetime.date(1967, 12, 31) result = baseline + datetime.timedelta(days=value) # Digits in a year YearStart = 0 YearFinish = 4 if("2" in rule): YearStart = 2 YearFinish = 4 delimiter = '/' if("-" in rule): delimiter = "-" if("Y" in rule): result = str(result.year)[YearStart:YearFinish] else: if("DM" in rule): result = str(result.month) else: if("DD" in rule): result = str(result.day) else: result = str(result.month) + delimiter + str(result.day) + delimiter + (str(result.year)[YearStart:YearFinish]) return result def oconv(value, rule): rule = rule.upper() if rule[0] == 'D': result = oconv_date(value,rule) if rule[0] == 'G': result = oconv_group(value,rule) if rule[0:2] == 'MT': result = oconv_time(value,rule) return result RUBY (Full code: https://github.com/CharlesBarouch/mv_core) # mv_core.rb # by Aaron Young (brainomite@gmail.com) # on 09/30/19 # ----------------------------------------- require "Date" def mv_oconv(value, rule) upcased_rule = rule.upcase # ensure rule is uppercase one_letter_rule = upcased_rule[0] two_letter_rule = upcased_rule[0..1] # get the first two letters using a range if one_letter_rule == "D" # its a date result = mv_oconv_date(value.to_i, upcased_rule) elsif one_letter_rule == "G" # its a group result = mv_oconv_group(value, upcased_rule) elsif two_letter_rule == "MT" # its a time result = mv_oconv_time(value.to_i, upcased_rule) else result = nil end result.to_s end def mv_oconv_date(value, rule) # create a date starting from 12/31/1967 and add value (days) to it date = Date.new(1967,12,31) + value case rule when "DM" date.strftime("%m") # zero padded month string when "DD" date.strftime("%d") # zero padded day string # regular expression for a full date with delimiters i.e. "D2-" when /D[1234][-\/]/ get_date(date, rule) # regular expression for year with a length i.e. "D4Y" when /D[1234]Y/ get_year(date, rule) end end NODEJS (Full code: https://github.com/CharlesBarouch/mv_core) // mvCore.js // by Aaron Young (brainomite@gmail.com) // on 10/13/19 // ----------------------------------------- const mvOconv = (value, rule) => { upcasedRule = rule.toUpperCase(); oneLetterRule = upcasedRule[0]; twoLetterRule = upcasedRule.substring(0, 2); if (oneLetterRule === "D") { return mvOconvDate(Number.parseInt(value), upcasedRule); } else if (twoLetterRule === "MT") { return mvOconvTime(Number.parseInt(value), upcasedRule); } else if (oneLetterRule === "G") { return mvOconvGroup(value, upcasedRule); } }; const mvOconvDate = (value, rule) => { const mvEpoch = new Date(1967, 11, 31); let date = mvEpoch.addDays(value); if (/D[1234][-\/]/.test(rule)) { // regular expression for a full date with delimiters i.e. "D2-" return getDate(date, rule); } else if (/D[1234]Y/.test(rule)) { // regular expression for year with a length i.e. "D4Y" return getYear(date, rule); } else if (rule === "DM") { return pad(date.getMonth() + 1); // zero based months } else if (rule === "DD") { return pad(date.getDate()); } else { return "oops"; } }; const getYear = (date, rule) => { years = date.getFullYear().toString(); chars = Number.parseInt(rule[1]); return years.substring(4 - chars); }; const getDate = (date, rule) => { day = pad(date.getDate()); month = pad(date.getMonth() + 1); // zero-based months need to add 1 year = getYear(date, rule); delim = rule[2]; return `${month}${delim}${day}${delim}${year}`; // return "yay"; }; // helpers const isInteger = string => Number.isInteger(Number.parseInt(string)); const pad = number => { if (number < 10) { return "0" + number; } return number.toString(); }; Date.prototype.addDays = function(days) { // https://stackoverflow.com/questions/563406/add-days-to-javascript-date var date = new Date(this.valueOf()); date.setDate(date.getDate() + days); return date; }; const findFirstNonNumericValue = value => { for (char of value) { if (!isInteger(char)) { return char; } } }; module.exports = { mvOconv }; C# (Full code: https://github.com/CharlesBarouch/mv_core) using System; using System.Globalization; namespace mv_core { public class mv_conv { public string mv_oconv(string value, string rule) { string result = ""; string rule1 = rule.ToUpper().Substring(0, 1); string rule2 = rule.Substring(0, 2); if (rule1 == "D") { result = mv_oconv_date(value, rule); } else if (rule2 == "MT") { result = mv_oconv_time(value, rule); } else if (rule1 == "G") { } else { result = ""; } return result; } private string mv_oconv_date(string value, string rule) { string result = ""; DateTime dt = new DateTime(1967, 12, 31); dt = dt.AddDays(Convert.ToInt16(value)); //break into elements string dt_day = dt.ToString("dd"); string dt_mo = dt.ToString("MM"); string dt_yr = dt.ToString("yyyy"); string dt_day_shortname = dt.ToString("ddd"); string dt_mo_shortname = dt.ToString("MMM"); string dt_yr2 = dt_yr.Substring(2, 2); string separator = rule.Contains("/") ? "/" : rule.Contains("-") ? "-" : " "; string toReturn = string.Concat("{0}", separator, "{1}", separator, "{2}"); switch (rule) { case "D2/": case "D2-": result = String.Format(toReturn, dt_mo, dt_day, dt_yr2); break; case "D2": case "D4": result = String.Format(toReturn, dt_day, dt_mo_shortname, (rule == "D2" ? dt_yr2 : dt_yr)); break; case "D4/": case "D4-": result = String.Format(toReturn, dt_mo, dt_day, dt_yr); break; case "DD": result = dt_day; break; case "DW": result = (Convert.ToInt32(dt.DayOfWeek) * 1).ToString(); break; case "DWA": result = dt.ToString("dddd"); break; case "DWB": result = dt_day_shortname; break; case "DM": result = dt_mo; break; case "DMA": result = dt.ToString("MMMM"); break; case "DMB": result = dt_mo_shortname; break; case "DQ": result = GetQuarter(dt).ToString(); break; case "DY": result = dt_yr; break; case "DY2": result = dt_yr2; break; case "DY4": result = dt_yr; break; } return result.ToUpper(); } }
Figure 1
TIME
As with Date, above, we'll just implement a subset of the features [Figure 2].
PHP function mv_oconv_time($value,$rule) { $hour = floor($value / 3600); $minute = floor(($value - $hour*3600)/ 60); $second = $value - ($hour*3600 + $minute*60); $apm = ''; if (substr($rule,2,1) == 'H') { $hour = ($hour % 24); if($hour >= '00' && $hour <= '11') {$apm = 'am';} else {$apm = 'pm'; $hour = $hour - 12;} } $hour = str_pad($hour, 2, "0", STR_PAD_LEFT ); $minute = str_pad($minute, 2, "0", STR_PAD_LEFT ); $second = str_pad($second, 2, "0", STR_PAD_LEFT ); $result = $hour . ':' . $minute . ':' . $second . $apm; return $result; } PYTHON def oconv_time(value,rule): result = datetime.timedelta(seconds=value) return str(result) RUBY def mv_oconv_time(value, rule) time = Time.at(value) # create a time object using seconds time.gmtime # remove utc offsets so it isn't skewed # convert to a string if rule == "MTS" # use military time time.strftime("%H:%M:%S") elsif rule == "MTHS" # use non-military time with a meridiem indicator time.strftime("%I:%M:%S%^P") else nil # return nothing, not a valid rule end end NODEJS const mvOconvTime = (value, rule) => { const time = new Date(value * 1000); // uses miliseconds const seconds = pad(time.getUTCSeconds()); const minutes = pad(time.getUTCMinutes()); if (rule === "MTS") { const hours = pad(time.getUTCHours()); return `${hours}:${minutes}:${seconds}`; } if (rule === "MTHS") { let hours; const utcHours = time.getUTCHours(); if (utcHours === 0) { hours = 12; } else if (utcHours > 12) { hours = pad(utcHours - 12); } else { hours = pad(utcHours); } const AMorPM = utcHours < 12 ? "AM" : "PM"; return `${hours}:${minutes}:${seconds}${AMorPM}`; } }; C# private string mv_oconv_time(string value, string rule) { string result = ""; Int32 value_time = Convert.ToInt32(value); Int32 hour = (value_time / 3600); Int32 minute = ((value_time - (hour * 3600)) / 60); Int32 second = ((value_time - ((hour * 3600) + (minute * 60))));
Figure 2
GROUP EXTRACT
Our Group Extract is slightly more robust than the MultiValue version. It will accept multiple character delimiters. Included in the code is notes on how to scale it back if you want single characters only [Figure 3].
PHP function mv_oconv_group($value,$rule) { // Split up the Rule into Skip, Delimiter, and Take $skip = 0; $take = 0; $dlmtr = ''; $rpos = 0; $smax = strlen($rule); for ($scnt = 1; $scnt < $smax; $scnt++) { $chr = $rule[$scnt]; if(is_numeric($chr)) { if($rpos == 0){$skip .= $chr;} else {$take .= $chr;} } else { if($dlmtr == ''){ $dlmtr = $chr; } $rpos = 2; } } $result = ''; $temp = explode($dlmtr,$value); $skip += 0; // Force numeric $rmax = $skip + $take; for($rcnt = $skip; $rcnt < $rmax; $rcnt++) { if($result != '') { $result .= $dlmtr;} $result .= $temp[$rcnt]; } return $result; } PYTHON def oconv_group(value,rule): # split rule into skip, delimiter, and take skip = 1 take = 3 delimiter = '!' # apply rule result = '' value = value.split(delimiter) for parts in value: if skip > 0: skip -= 1 else: if take > 0: take -= 1 if result != '': result += delimiter result += parts return result RUBY def mv_oconv_group(value, rule) actual_rule = rule[1..-1] # remove first char delimiter = find_first_non_numeric_value(actual_rule) # find the delimiter # take the rule and turn into an array using the delimiter then # convert all elements into integers and assign the first # value to skip_num and second value to take_num skip_num, take_num = actual_rule.split(delimiter).map(&:to_i) array = value.split(delimiter) # create an array using the delimiter # create a sub array by skipping skip_num numbers then take the first # take_num elements and return the new resulting array array[skip_num..-1].take(take_num) end NODEJS const mvOconvGroup = (value, rule) => { const actualRule = rule.substring(1); const delimiter = findFirstNonNumericValue(actualRule); const [skip_num, take_num] = actualRule .split(delimiter) .map(val => Number.parseInt(val)); const fullArray = value.split(delimiter); const subArray = fullArray.slice(skip_num); const resultArray = subArray.slice(0, take_num); return resultArray.toString(); }; C# Forthcoming
Figure 3
EXAMPLES OF USE
See Figure 4.
PHP <?php // add the function to this script include_once('./mv_core.php'); // // Load Test Cases $stack = file_get_contents('../teststack.txt'); $stack = explode('^',$stack); echo 'Loaded Test Cases' . "\r\n"; foreach($stack as $testcase) { $testcase = explode(']',$testcase); echo mv_oconv($testcase[0],$testcase[1]) . "\r\n"; } // // Run some pre-set cases echo "\r\n"; echo 'Hardcoded Cases' . "\r\n"; echo mv_oconv(-1200,'D2/') . "\r\n"; echo mv_oconv(18500,'D2/') . "\r\n"; echo mv_oconv(18500,'D4-') . "\r\n"; echo mv_oconv(18500,'DM') . "\r\n"; echo mv_oconv(18500,'DD') . "\r\n"; echo mv_oconv(18500,'D2Y') . "\r\n"; echo mv_oconv(86375,'MTS') . "\r\n"; echo mv_oconv(86375,'MTHS') . "\r\n"; echo mv_oconv('A!BB!CCC!DDD!DDD','G1!3'); ?> PYTHON import mv_core as mv print(mv.oconv(18575,'D2/')) print(mv.oconv(18575,'D4-')) print(mv.oconv(18575,'DM')) print(mv.oconv(18575,'DD')) print(mv.oconv(18575,'D2Y')) print(mv.oconv(86375,'MTS')) print(mv.oconv(86375,'MTHS')) print(mv.oconv('A!BB!CCC!DDD!DDD','G1!3')) RUBY # test.rb # by Aaron Young (brainomite@gmail.com) # on 09/30/19 # ----------------------------------------- require_relative "mv_core.rb" puts "Loaded Test Cases" file = File.open(__dir__ + "/../teststack.txt") # read the file and remove linefeeds stack_data = file_data = file.read.chomp tests = stack_data.split("^") tests.each do |test| params = test.split("]") value = params[0] rule = params[1] expected = params[2] puts "mv_oconv(#{value}, \"#{rule}\") - Expected: '#{expected}' - Actual: '#{mv_oconv(value, rule)}'" end puts "" # Run some pre-set cases puts "Hardcoded Cases" puts mv_oconv(18500,'D2/') # 08/25/18 puts mv_oconv(18500,'D4-') # 08-25-2018 puts mv_oconv(18500,'DM') # 08 puts mv_oconv(18500,'DD') # 25 puts mv_oconv(18500,'D2Y') # 18 puts mv_oconv(86375,'MTS') # 23:59:35 puts mv_oconv(86375,'MTHS') # 11:59:35pm puts mv_oconv('A!BB!CCC!DDD!DDD','G1!3') # ["BB", "CCC", "DDD"] NODEJS // by Aaron Young (brainomite@gmail.com) // on 09/30/19 // ----------------------------------------- const { mvOconv } = require("./mvCore"); const path = require("path"); const filePath = path.join(__dirname, "..", "teststack.txt"); console.log("Loaded Test Cases"); const stackData = require("fs") .readFileSync(filePath, "utf-8") .split("\n") .filter(Boolean)[0]; // get first line sans new line chars tests = stackData.split("^"); for (test of tests) { const [value, rule, expected] = test.split("]"); const result = mvOconv(value, rule); console.log( `mv_oconv(${value}, "${rule}") - Expected: '${expected}' - Actual: '${result}'` ); } console.log(""); console.log("Hardcoded Cases"); console.log(mvOconv(18500, "D2/")); // 08/25/18 console.log(mvOconv(18500, "D4-")); // 08-25-2018 console.log(mvOconv(18500, "DM")); // 08 console.log(mvOconv(18500, "DD")); // 25 console.log(mvOconv(18500, "D2Y")); // 18 console.log(mvOconv(86375, "MTS")); // 23:59:35 console.log(mvOconv(86375, "MTHS")); // 11:59:35PM console.log(mvOconv("A!BB!CCC!DDD!DDD", "G1!3")); // BB,CCC,DDD C# using System; using mv_core; namespace mv_oconv_test { class Program { static void Main(string[] args) { string test_date = "18915"; var conv = new mv_core.mv_conv(); string oconv_date = conv.mv_oconv(test_date, "D2/"); Console.WriteLine("D2/ - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "D2-"); Console.WriteLine("D2- - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "D2"); Console.WriteLine("D2 - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "D4/"); Console.WriteLine("D4/ - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "D4-"); Console.WriteLine("D4- - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "D4"); Console.WriteLine("D4 - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DD"); Console.WriteLine("DD - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DW"); Console.WriteLine("DW - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DWA"); Console.WriteLine("DWA - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DWB"); Console.WriteLine("DWB - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DM"); Console.WriteLine("DM - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DMA"); Console.WriteLine("DMA - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DMB"); Console.WriteLine("DMB - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DQ"); Console.WriteLine("DQ - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DY"); Console.WriteLine("DY - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DY2"); Console.WriteLine("Dy2 - " + oconv_date); oconv_date = conv.mv_oconv(test_date, "DY4"); Console.WriteLine("DY4 - " + oconv_date); string test_time = "12519"; string oconv_time = conv.mv_oconv(test_time, "MT"); Console.WriteLine("MTS - " + oconv_time); oconv_time = conv.mv_oconv(test_time, "MTS"); Console.WriteLine("MT - " + oconv_time); oconv_time = conv.mv_oconv(test_time, "MTHS"); Console.WriteLine("MTHS- " + oconv_time); var ans = Console.ReadLine(); } } }
Figure 4
Next article, we'll build on these examples and talk more about how to use them to reach Ruby, C#, Python, Node.js, and PHP people how MultiValue works. By then, we may have a few more languages added.
You can find this code on GitHub: https://github.com/CharlesBarouch/mv_core . You can also create a branch and start adding features and corrections. We welcome your participation.