DynamicOps.java

/*
** Module   : DynamicOps.java
** Abstract : operators and functions that have dynamic type behavior
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description---------------------------------
** 001 GES 20130301 Created the initial version with support for a version of the plus and minus
**                  operators which dynamically handles the return type based on the operands.
** 002 OM  20130321 Added support for LENGTH function when the 1st parameter type cannot be 
**                  detected at conversion time.
** 003 VIG 20131010 Added null processing for type argument in length-methods
** 004 MAG 20140606 Implements plus(BDT,BDT) and minus(BDT,BDT) binary operators.
** 005 OM  20160711 Switch to int64 as return type of length() methods.
** 006 CA  20160919 Added APIs for divide, multiply, modulo and negate.
** 007 CA  20160924 Fixed a problem in H006 which refactored the validity of the operands for the
**                  plus and minus operators: the entire expression needs to be negated, not just
**                  the first operand.
** 008 EVL 20161003 Making transformation attempt from character to numeric as possible fix for
**                  operands compatibility issues.
** 009 CA  20181221 Added multiple(NumberType, NumberType).
** 010 CA  20190509 MIN and MAX can be POLY, too.
** 011 CA  20190826 Fixed string concatenation, where toStringMessage() must be used, and not 
**                  toString().
** 012 CA  20200503 DynamicOps.length must return integer and not int64.
**     AL2 20220319 Added value proxy check.
** 013 CA  20230116 A dynamic DIVIDE always returns a decimal.
** 014 AS  20241120 Fixed the length method for some BDTs.
** 015 ICP 20250123 Used integer.of and int64.of to leverage caches instances.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import java.math.*;

/**
 * A class that provides Progress 4GL compatible operators and functions that have dynamic
 * type behavior. This class provides all functionality as statics and has no instance data.
 */
public final class DynamicOps
{
   /** character type in Progress */
   static final String CHAR_TYPE = "character";

   /** Data types to return from dynamic function. */
   static enum ReturnType
   {
      INTEGER, DECIMAL, INT64
   }
   
   /**
    * Returns the largest instance from the list.  All the values must be assignment-compatible.
    * If they are not, then {@link #IncompatibleDataTypesInExpression()} is executed.
    *
    * @param    args
    *           The list of instances to compare.
    *
    * @return   The largest instance or <code>unknown value</code> if any entries in the list are 
    *           unknown or <code>null</code> if an empty list is passed.
    */
   public static BaseDataType maximum(BaseDataType... args)
   {
      if (args == null || args.length == 0)
      {
         return new unknown();
      }
      
      if (args.length == 1)
      {
         return args[0].val();
      }
      
      // the argument may be a proxy; make sure to unwrap and work with the real value
      Class<?> type = args[0].val().getClass();
      BaseDataType max = args[0].val();
      BaseDataType val = BaseDataType.generateUnknown(type);
      for (int i = 1; i < args.length; i++)
      {
         if (!compatibleDataTypes(type, args[i].val().getClass()))
         {
            IncompatibleDataTypesInExpression();
            break;
         }
         
         val.assign(args[i].val());
         
         if (CompareOps._isGreaterThan(val, max))
         {
            max.assign(val);
         }
      }
      
      return max;
   }
   
   /**
    * Returns the smallest instance from the list.  All the values must be assignment-compatible.
    * If they are not, then {@link #IncompatibleDataTypesInExpression()} is executed.
    *
    * @param    args
    *           The list of instances to compare.
    *
    * @return   The smallest instance or <code>unknown value</code> if any entries in the list are 
    *           unknown or <code>null</code> if an empty list is passed.
    */
   public static BaseDataType minimum(BaseDataType... args)
   {
      if (args == null || args.length == 0)
      {
         return new unknown();
      }
      
      if (args.length == 1)
      {
         return args[0].val();
      }

      // the argument may be a proxy; make sure to unwrap and work with the real value
      Class<?> type = args[0].val().getClass();
      BaseDataType min = args[0].val();
      BaseDataType val = BaseDataType.generateUnknown(type);
      for (int i = 1; i < args.length; i++)
      {
         if (!compatibleDataTypes(type, args[i].val().getClass()))
         {
            IncompatibleDataTypesInExpression();
            break;
         }
         
         val.assign(args[i].val());
         
         if (CompareOps._isLessThan(val, min))
         {
            min.assign(val);
         }
      }
      
      return min;
   }
   
   /**
    * Implements the binary plus operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand.
    *
    * @return   The dynamically determined result.
    */
   public static BaseDataType plus(BaseDataType op1, BaseDataType op2)
   {
      // the operands may be proxy; make sure to unwrap and work with the real values
      op1 = op1 == null ? null : op1.val();
      op2 = op2 == null ? null : op2.val();
      
      // allowed types
      if (!(((op1 instanceof Text)       ||
             (op1 instanceof NumberType) ||
             (op1 instanceof date)       ||
             (op1 instanceof handle)     ||
             (op1 instanceof logical)) &&
            ((op2 instanceof Text)       ||
             (op2 instanceof NumberType) ||
             (op2 instanceof date)       ||
             (op2 instanceof handle)     ||
             (op2 instanceof logical))))
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }

      // check for unknown results
      if (op1.isUnknown() || op2.isUnknown())
      {
         return op1.instantiateUnknown();
      }
      
      // P4G restrictions.
      // handle or logical are valid only with Text values.
      if (!(op1 instanceof Text) && (op2 instanceof handle || op2 instanceof logical))
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }
      else if (!(op2 instanceof Text) && (op1 instanceof handle || op1 instanceof logical))
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }      
      // datetime and datetimetz cannot be used. 
      else if (op1 instanceof datetime || op2 instanceof datetime)
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }
      // any date and date not allowed.
      else if (op1 instanceof date && op2 instanceof date)
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }
      
      // at least one operand is longchar type.
      if (op1 instanceof longchar || op2 instanceof longchar)
      {
         String val1 = new longchar(op1).toStringMessage();
         String val2 = new longchar(op2).toStringMessage();
         
         // concatenate values
         StringBuilder sb = new StringBuilder(val1);
         sb.append(val2);
         return new longchar(sb.toString());
      }
      // at least one operand is character type.
      else if (op1 instanceof character || op2 instanceof character)
      {
         String val1 = new character(op1).toStringMessage();
         String val2 = new character(op2).toStringMessage();
         
         // concatenate values
         StringBuilder sb = new StringBuilder(val1);
         sb.append(val2);
         return new character(sb.toString());
      }
      // at least one operand is date type.
      else if (op1 instanceof date || op2 instanceof date)
      {
         long val1 = new date(op1).longValue();
         long val2 = new date(op2).longValue();
         return new date(val1 + val2);
      }
      // at least one operand is decimal type.
      else if (op1 instanceof decimal || op2 instanceof decimal)
      {
         BigDecimal val1 = new decimal(op1).toBigDecimal();
         BigDecimal val2 = new decimal(op2).toBigDecimal();
         return new decimal(val1.add(val2));
      }
      // at least one operand is int64 type.
      else if (op1 instanceof int64 || op2 instanceof int64)
      {
         long val1 = new int64(op1).longValue();
         long val2 = new int64(op2).longValue();
         return int64.of(val1 + val2);
      }
      // at least one operand is integer type.
      else if (op1 instanceof integer || op2 instanceof integer)
      {
         long val1 = new integer(op1).longValue();
         long val2 = new integer(op2).longValue();
         return integer.of(val1 + val2);
      }
      // at least one operand is recid type.
      else if (op1 instanceof recid || op2 instanceof recid)
      {
         long val1 = new recid(op1).longValue();
         long val2 = new recid(op2).longValue();
         return new recid(val1 + val2);
      }
      else
      {
         IncompatibleDataTypesInExpression();
      }
      
      return new unknown();
   }

   /**
    * Implements the minus binary operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand.
    *
    * @return   The dynamically determined result.
    */
   public static BaseDataType minus(BaseDataType op1, BaseDataType op2)
   {
      // the operands may be proxy; make sure to unwrap and work with the real values
      op1 = op1 == null ? null : op1.val();
      op2 = op2 == null ? null : op2.val();
      
      // allowed BDT types
      if (!(((op1 instanceof NumberType) ||
             (op1 instanceof date)       ||
             (op1 instanceof handle)     ||
             (op1 instanceof logical)) &&
            ((op2 instanceof NumberType) ||
             (op2 instanceof date)       ||
             (op2 instanceof handle)     ||
             (op2 instanceof logical))))
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }

      // check for unknown results
      if (op1.isUnknown() || op2.isUnknown())
      {
         return op1.instantiateUnknown();
      }

      // P4G restrictions.
      // datetime and datetimetz cannot be used. 
      if (op1 instanceof datetime || op2 instanceof datetime)
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }
      // handle or logical works only with decimals.
      else if (!(op1 instanceof decimal) && (op2 instanceof handle || op2 instanceof logical))
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }
      else if (!(op2 instanceof decimal) && (op1 instanceof handle || op1 instanceof logical))
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }      
      // date as op2 works only with date or decimal as op1
      else if (op2 instanceof date && !(op1 instanceof date || op1 instanceof decimal))
      {
         IncompatibleDataTypesInExpression();
         return new unknown();
      }
            
      // at least one operand is decimal type.
      if (op1 instanceof decimal || op2 instanceof decimal)
      {
         BigDecimal val1 = new decimal(op1).toBigDecimal();
         BigDecimal val2 = new decimal(op2).toBigDecimal();
         return new decimal(val1.subtract(val2));
      }
      // both operands are date but the return type is decimal!!!
      else if (op1 instanceof date && op2 instanceof date)
      {
         long val1 = new date(op1).longValue();
         long val2 = new date(op2).longValue();
         return new decimal(val1 - val2);         
      }
      // only op1 could be of type date
      else if (op1 instanceof date)
      {
         long val1 = new date(op1).longValue();
         long val2 = new date(op2).longValue();
         return new date(val1 - val2);         
      }
      // at least one operand is int64 type.
      else if (op1 instanceof int64 || op2 instanceof int64)
      {
         long val1 = new int64(op1).longValue();
         long val2 = new int64(op2).longValue();
         return int64.of(val1 - val2);
      }
      // at least one operand is integer type.
      else if (op1 instanceof integer || op2 instanceof integer)
      {
         long val1 = new integer(op1).longValue();
         long val2 = new integer(op2).longValue();
         return integer.of(val1 - val2);
      }
      // at least one operand is recid type.
      else if (op1 instanceof recid || op2 instanceof recid)
      {
         long val1 = new recid(op1).longValue();
         long val2 = new recid(op2).longValue();
         return new recid(val1 - val2);
      }
      else
      {
         IncompatibleDataTypesInExpression();
      }
      
      return new unknown();
   }

   /**
    * Error message to display for incompatible types. 
    */
   private static void IncompatibleDataTypesInExpression()
   {
      String err = "Incompatible data types in expression or assignment.";
      ErrorManager.recordOrThrowError(223, err);
   }
      
   /**
    * Returns the length of the given <code>BaseDataType</code> instance. This method is called 
    * when P2J is unable to resolve the type of the first parameter at conversion time and this
    * is postponed to runtime when the actual type of <code>bdt</code> will be determined 
    * dynamically.
    *
    * @param   bdt
    *          The instance to test.
    * @param   type
    *          The measuring unit (CHARACTER or RAW, COLUMN not supported yet).
    *
    * @return  The number of characters or bytes in the given instance or <code>unknown</code>
    *          value  if the instance is <code>unknown</code>.
    */
   public static integer length(BaseDataType bdt, character type)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      bdt = bdt == null ? null : bdt.val();
      
      if (type == null || type.isUnknown())
      {
         return integer.UNKNOWN;
      }
      
      return DynamicOps.length(bdt, type.toStringMessage());
   }
   
   /**
    * Returns the length of the given <code>BaseDataType</code> instance. This method is called 
    * when P2J is unable to resolve the type of the first parameter at conversion time and this
    * is postponed to runtime when the actual type of <code>bdt</code> will be determined 
    * dynamically.
    *
    * @param   bdt
    *          The instance to test.
    * @param   type
    *          The measuring unit (CHARACTER or RAW, COLUMN not supported yet).
    *          "character" is supposed to be the default measurement unit 
    *           for the LENGTH builtin function (when type is null)
    *
    * @return  The number of characters or bytes in the given instance or <code>unknown</code>
    *          value  if the instance is <code>unknown</code>.
    */
   public static integer length(BaseDataType bdt, String type)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      bdt = bdt == null ? null : bdt.val();
      
      if (bdt == null || bdt.isUnknown())
      {
         return integer.UNKNOWN;
      }
      
      if (type == null)
      {
         type = CHAR_TYPE;
      }
      
      if (bdt instanceof Text)
      {
         Text txt = (Text) bdt;
         return TextOps.length(txt, type);
      }
      else if (bdt instanceof BinaryData)
      {
         return new integer(BinaryData.length((BinaryData) bdt));
      }
      else if (bdt instanceof logical)
      {
         if (((logical) bdt).booleanValue())
         {
            return integer.of(3);
         }
         else
         {
            return integer.of(2);
         }
      }
      else if (bdt instanceof datetimetz)
      {
         ErrorManager.recordOrThrowError(5730,
                 "Unable to complete automatic conversion to character -- use STRING function", false);
      }
      else if (bdt instanceof NumberType || bdt instanceof date || bdt instanceof handle)
      {
         return integer.of(bdt.toStringExport().length());
      }
      else if (bdt instanceof rowid)
      {
         ErrorManager.recordOrThrowError(5729,
                 "Incompatible datatypes found during runtime conversion", false);
      }

      // none of the above cases ?
      return integer.UNKNOWN;
   }
   
   /**
    * Returns the length of the given <code>BaseDataType</code> instance. This method is called 
    * when P2J is unable to resolve the type of the first parameter at conversion time and this
    * is postponed to runtime when the actual type of <code>bdt</code> will be determined 
    * dynamically.
    *
    * @param   bdt
    *          The instance to test.
    *
    * @return  The number of characters or bytes in the given instance or <code>unknown</code>
    *          value  if the instance is <code>unknown</code>.
    */
   public static integer length(BaseDataType bdt)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      bdt = bdt == null ? null : bdt.val();
      // if [cpinternal] is set to [UTF-8] and the [type] is invalid, the following error condition is thrown:
      //       ** The data type argument value must be "raw", "character" or "column". (1186)
      return DynamicOps.length(bdt, CHAR_TYPE);
   }
   
   /**
    * Implements the binary divide operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand.
    *
    * @return   The dynamically determined result.
    */
   public static decimal divide(BaseDataType op1, BaseDataType op2)
   {
      // the operands may be proxy; make sure to unwrap and work with the real values
      op1 = op1 == null ? null : op1.val();
      op2 = op2 == null ? null : op2.val();
      
      // try to transform character values to number one
      if (op1 instanceof character)
      {
         op1 = characterToNumber(op1, ReturnType.DECIMAL);
      }
      if (op2 instanceof character)
      {
         op2 = characterToNumber(op2, ReturnType.DECIMAL);
      }
      
      // allowed types
      if (!(op1 instanceof NumberType && op2 instanceof NumberType))
      {
         IncompatibleDataTypesInExpression();
         return new decimal();
      }

      // check for unknown results
      if (op1.isUnknown() || op2.isUnknown())
      {
         return new decimal();
      }
      
      return MathOps.divide(((NumberType) op1), ((NumberType) op2));
   }
   
   /**
    * Implements the binary multiply operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.  If it is character attempt to convert to numeric happens.
    * @param    op2
    *           The right operand.
    *
    * @return   The dynamically determined result.
    */
   public static NumberType multiply(NumberType op1, NumberType op2)
   {
      // the values may be proxy; make sure to unwrap and work with the real values
      return multiply(op1 == null ? null : op1.val(), op2 == null ? null : op2.val());
   }

   /**
    * Implements the binary multiply operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.  If it is character attempt to convert to numeric happens.
    * @param    op2
    *           The right operand.
    *
    * @return   The dynamically determined result.
    */
   public static NumberType multiply(BaseDataType op1, NumberType op2)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      op1 = op1 == null ? null : op1.val();
      BaseDataType val2 = op2 == null ? null : op2.val();
      
      // try to transform character values to number one
      if (op1 instanceof character)
      {
         if (val2 instanceof decimal)
         {
            op1 = characterToNumber(op1, ReturnType.DECIMAL);
         }
         else if (val2 instanceof integer)
         {
            op1 = characterToNumber(op1, ReturnType.INTEGER);
         }
      }

      // and call the base version
      return multiply(op1, val2);
   }
   
   /**
    * Implements the binary multiply operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand.  If it is character attempt to convert to numeric happens.
    *
    * @return   The dynamically determined result.
    */
   public static NumberType multiply(NumberType op1, BaseDataType op2)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      BaseDataType val1 = op1 == null ? null : op1.val();
      op2 = op2 == null ? null : op2.val();
      
      // try to transform character values to number one
      if (op2 instanceof character)
      {
         if (val1 instanceof decimal)
         {
            op2 = characterToNumber(op2, ReturnType.DECIMAL);
         }
         else if (val1 instanceof integer)
         {
            op2 = characterToNumber(op2, ReturnType.INTEGER);
         }
      }

      // and call the base version
      return multiply(val1, op2);
   }
   
   /**
    * Implements the binary multiply operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand.
    *
    * @return   The dynamically determined result.
    */
   public static NumberType multiply(BaseDataType op1, BaseDataType op2)
   {
      // the values may be proxy; make sure to unwrap and work with the real values
      op1 = op1 == null ? null : op1.val();
      op2 = op2 == null ? null : op2.val();
      
      // allowed types
      if (!(op1 instanceof NumberType && op2 instanceof NumberType))
      {
         IncompatibleDataTypesInExpression();
         return new decimal();
      }

      // check for unknown results
      if (op1.isUnknown() || op2.isUnknown())
      {
         return new decimal();
      }
      
      return MathOps.multiply(((NumberType) op1), ((NumberType) op2));
   }

   /**
    * Implements the binary modulo operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand.
    *
    * @return   The dynamically determined result.
    */
   public static NumberType modulo(BaseDataType op1, BaseDataType op2)
   {
      // the values may be proxy; make sure to unwrap and work with the real values
      op1 = op1 == null ? null : op1.val();
      op2 = op2 == null ? null : op2.val();
      
      // try to transform character values to number one
      if (op1 instanceof character)
      {
         op1 = characterToNumber(op1, ReturnType.INTEGER);
      }
      if (op2 instanceof character)
      {
         op2 = characterToNumber(op2, ReturnType.INTEGER);
      }
      
      // allowed types
      if (!(op1 instanceof NumberType && op2 instanceof NumberType))
      {
         IncompatibleDataTypesInExpression();
         return new decimal();
      }

      // check for unknown results
      if (op1.isUnknown() || op2.isUnknown())
      {
         return new decimal();
      }
      
      return MathOps.modulo(((NumberType) op1), ((NumberType) op2));
   }

   /**
    * Implements the unary negate operator whose result will differ based on the types of the
    * operands, which will not be known until runtime.
    * <p>
    * If either operand is the <code>unknown value</code>, the result will
    * be the <code>unknown value</code>.
    *
    * @param    op
    *           The operand.
    *
    * @return   The dynamically determined result.
    */
   public static NumberType negate(BaseDataType op)
   {
      // the operand may be a proxy; make sure to unwrap and work with the real value
      op = op == null ? null : op.val();
      // allowed types
      if (!(op instanceof NumberType) || op.isUnknown())
      {
         IncompatibleDataTypesInExpression();
         return new decimal();
      }
      
      if (op instanceof decimal)
      {
         return MathOps.negate((decimal) op);
      }
      else
      {
         return MathOps.negate((int64) op);
      }
   }

   /**
    * Helper method to make transformation attempt from character data type into numeric one.
    * <p>
    * Can be used as last resort if for some reasons operant types are not compatible.
    *
    * @param    bdt
    *           The value to convert.
    * @param    type
    *           The converted variable type.
    *
    * @return   The new value of number type if conversion is possible or original value if not.
    */
   private static BaseDataType characterToNumber(BaseDataType bdt, ReturnType type)
   {
      if (bdt.isUnknown())
      {
         return bdt;
      }
      
      BaseDataType bdtRet = null;
      
      // trying to convert non-emtpty string representation to decimal
      // depending on data type requested
      String bdtString = bdt.toStringMessage();
      if (!bdtString.isEmpty())
      {
         switch (type)
         {
            case DECIMAL:
            {
               bdtRet = new decimal(bdtString);
               break;
            }
            case INTEGER:
            {
               bdtRet = new integer(bdtString);
               break;
            }
         }
      }
      
      return bdtRet != null ? bdtRet : bdt;
   }
   
   /**
    * Check if the given 4GL data types are assignment-compatible.
    * 
    * @param    bdt1
    *           The first data type to check.
    * @param    bdt2
    *           The first data type to check.
    *           
    * @return   <code>true</code> if the types are compatible.
    */
   private static boolean compatibleDataTypes(Class<?> bdt1, Class<?> bdt2)
   {
      if (!(BaseDataType.class.isAssignableFrom(bdt1) && 
            BaseDataType.class.isAssignableFrom(bdt2)))
      {
         return false;
      }
      if (bdt1 == bdt2 || bdt2.isAssignableFrom(bdt1) || bdt1.isAssignableFrom(bdt2))
      {
         return true;
      }
      
      if (BinaryData.class.isAssignableFrom(bdt1) && BinaryData.class.isAssignableFrom(bdt2))
      {
         return true;
      }
      
      if (date.class.isAssignableFrom(bdt1) && date.class.isAssignableFrom(bdt2))
      {
         return true;
      }
      
      if (NumberType.class.isAssignableFrom(bdt1) && NumberType.class.isAssignableFrom(bdt2))
      {
         return true;
      }
      
      if (Text.class.isAssignableFrom(bdt1) && Text.class.isAssignableFrom(bdt2))
      {
         return true;
      }
      
      if ((NumberType.class.isAssignableFrom(bdt1) && date.class.isAssignableFrom(bdt2)) ||
          (date.class.isAssignableFrom(bdt1) && NumberType.class.isAssignableFrom(bdt2)))
      {
         return true;
      }
      
      return false;
   }
}