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.

CHARLES BAROUCH

Charles Barouch is the CTO of HDWP, Inc. He is also a regular contributor to International Spectrum Magazine, a former Associate Editor for both Database Trends and for Gateways Magazine, a former distance learning Instructor for CALC. He is presently the Past President of the U2UG. Mr. Barouch has presented technology and business topics in front of hundreds of companies, in a wide range of product and service categories. He is available for on-site speaking and consulting engagements in and out of the United States.

View more articles

Featured:

Sep/Oct 2019

menu
menu