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.

