# Mon, 22 Dec 2025 14:57:12 GMT
---
version: 23
data:


- type: ModificationMethod
  attributes:
    displayName: Modify time data
    description: |
          Change a time data value by modifying it or generate or set a new value.
          Input can be a date, time, timestamp or string column.
          This method internally parses each of the input values to a timestamp and then allows to modify each part of this timestamp separately.
          For each of the parts (year, month, day, hour, minute, second, fractions) one of the following values can be specified:  * <value>            -> Set the specified value
            * +<value>           -> to add the specified amount to this part
            * -<value>           -> to subtract a specified amount from this part
            * NOW                -> take the value of the current timestamp
            * NOW+<value>        -> to add a given amount to the current timestamps part and use it
            * NOW-<value>        -> to subtract a given amount from the current timestamps part and use it
            * SEED(<min>:<max>)  -> Generate a new value for this part based on a given seed.
                                    The seed can be a column name of the table to use the columns value.
                                    Otherwise the seed value will be used as it is.
                                    The part in brackets is optional and can limit the range of the generated value.
                                    <min> -> specifies the low limit for the generated value.
                                    <max> -> specifies the high limit for the generated value.
                                    Both <min> and <max> are optional. If not specified the default value for the limit will be used.
                                    The seed value has to be specified in parameter seed.
            * +SEED(<min>:<max>) -> Generate a new value like SEED(<min>:<max>) but this value will be added to the current value of the part.
            * -SEED(<min>:<max>) -> Generate a new value like SEED(<min>:<max>) but this value will be subtracted from the current value of the part.
            * RAND(<min>:<max>)  -> Generate a new random value.
                                    The part in brackets is optional and can limit the range of the generated value.
                                    <min> -> specifies the low limit for the generated value.
                                    <max> -> specifies the high limit for the generated value.
                                    Both <min> and <max> are optional. If not specified the default value for the limit will be used.
                                    The default limits depend on the part of the timestamp.
            * +RAND(<min>:<max>) -> Generate a new value like RAND(<min>:<max>) but this value will be added to the current value of the part.
            * -RAND(<min>:<max>) -> Generate a new value like RAND(<min>:<max>) but this value will be subtracted from the current value of the part.
    inputType: COLUMN
    mappingTableContainers: []
    mappingTableAccessMode: READ
    code: |
        /*
         * *****************************************************************************
         * Modify Time Data
         * *****************************************************************************
         *
         * The following variables are available in this script:
         * =====================================================
         * ctx : A context object that offers a number of methods.
         *   The method indexOf() can be used to get the index of a
         *   specific column within the dataArray.
         * dataArray : The data array. This array contains the contents of all
         *   columns of the current row of the table that is being
         *   masked by this method.
         *
         * Additionally for column based modification methods the following two
         * variables are available:
         * columnName : The name of the column that matched the pattern.
         * columnIndex : The position of the column that matched the pattern within
         *   the data array. This value is zero-based.
         * *****************************************************************************
         */
        
        importPackage(Packages.java.util);
        importPackage(Packages.java.text);
        importClass(Packages.de.ubs.xdm3.batch.modification.ModificationException);
        importClass(Packages.de.ubs.xdm.utils.core.HighPrecisionTimestamp);
        importClass(Packages.de.ubs.xdm.utils.core.HighPrecisionTimestampFormat);
        importClass(Packages.de.ubs.xdm3.batch.modification.HashUtils);
        
        var cal;
        var calNow;
        var yearArray = {name:"YEAR", modify:false, min:1, max:9999, value:null, direction:null, baseValue:null};
        var monthArray = {name:"MONTH", modify:false, min:1, max:12, value:null, direction:null, baseValue:null};
        var dayArray = {name:"DAY", modify:false, min:1, max:31, value:null, direction:null, baseValue:null};
        var hourArray = {name:"HOUR", modify:false, min:0, max:23, value:null, direction:null, baseValue:null};
        var minuteArray = {name:"MINUTE", modify:false, min:0, max:59, value:null, direction:null, baseValue:null};
        var secondArray = {name:"SECOND", modify:false, min:0, max:59, value:null, direction:null, baseValue:null};
        var fractionArray = {name:"FRACTION", modify:false, min:0, max:999999999999, value:null, direction:null, baseValue:null};
        var seedType = "VALUE";
        var seedValue = null;
        var newFraction = 0;
        var hpt = null;
        
        /*
         * This function is called once before the processing starts. The method
         * is called once per table.
         */
        function init() {
            /* Read the input parameters */
            yearArray = checkInput(year, yearArray);
            monthArray = checkInput(month, monthArray);
            dayArray= checkInput(day, dayArray);
            hourArray = checkInput(hour, hourArray);
            minuteArray = checkInput(minute, minuteArray);
            secondArray = checkInput(second, secondArray);
            fractionArray = checkInput(fraction, fractionArray);
        
        }
        
        /*
         * This function should modify the row that is provided by the dataArray. The
         * method is called for each single row of the table.
         */
        function apply() {
        
            if (ctx.indexOf(seed) != -1) {
                seedValue = dataArray[ctx.indexOf(seed)];
            } else {
                if (seed == "RAND") {
                    seedValue = Math.floor(Math.random() * 1000000000);
                } else {
                    seedValue = seed;
                }
            }
        
            if (dataArray[columnIndex] == null && ignoreNull) {
                return;
            }
            var inputIsString = false;
            /* Read the HighPrecisionTimestamp from the table
             * If it is null use a new calendar
             * otherwise get the calendar from this HighPrecisionTimestamp to work with
             */
            cal = new GregorianCalendar();
            hpt = new HighPrecisionTimestamp(cal.getTime());
        
            var hptFormat = new HighPrecisionTimestampFormat(formatPattern, fractionPattern);
            var columnValue = dataArray[columnIndex];
            if (columnValue != null) {
                if (typeof columnValue === 'string' || columnValue instanceof String) {
                    inputIsString = true;
                    if (columnValue != "") {
                        hpt = hptFormat.parse(columnValue);
                        cal.setTime(hpt);
                        if (hpt.isHasTimezone()) {
                            cal.setTimeZone(TimeZone.getTimeZone(hpt.getTimeZone()));
                        }
                    }
                } else {
                    hpt = columnValue;
                    cal.setTime(hpt);
                    if (hpt.isHasTimezone()) {
                        cal.setTimeZone(TimeZone.getTimeZone(hpt.getTimeZone()));
                    }
                }
            }
            /* Get the current timestamp, and set it to the specified values */
            cal.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
        
            // Use this calNow Calender and set it to the target value,
            // because this calender uses the current LOCALE.
            // HighPrecisionTimestamp internally always sets the values to UTC
            calNow = new GregorianCalendar();
        
            /* Read the seed value from the table if a column name is specified as seed */
            var resultHcp = null;
            var useSeedOrRand = false;
            if (yearArray.baseValue == "SEED" || yearArray.baseValue == "RAND") { useSeedOrRand = true;};
            if (monthArray.baseValue == "SEED" || monthArray.baseValue == "RAND") { useSeedOrRand = true;};
            if (dayArray.baseValue == "SEED" || dayArray.baseValue == "RAND") { useSeedOrRand = true;};
            if (hourArray.baseValue == "SEED" || hourArray.baseValue == "RAND") { useSeedOrRand = true;};
            if (minuteArray.baseValue == "SEED" || minuteArray.baseValue == "RAND") { useSeedOrRand = true;};
            if (secondArray.baseValue == "SEED" || secondArray.baseValue == "RAND") { useSeedOrRand = true;};
            if (fractionArray.baseValue == "SEED" || fractionArray.baseValue == "RAND") { useSeedOrRand = true;};
        
            var numTries = 9999;
            if (!useSeedOrRand) {numTries = 1;}
            for (var offset = 0; offset < numTries && (resultHcp == null || hpt.compareTo(resultHcp) == 0); ++offset) {
                /* The timestamp parts with the new values */
                setCalNow(yearArray, Calendar.YEAR, offset);
                setCalNow(monthArray, Calendar.MONTH, offset);
                setCalNow(dayArray, Calendar.DAY_OF_MONTH, offset);
                setCalNow(hourArray, Calendar.HOUR_OF_DAY, offset);
                setCalNow(minuteArray, Calendar.MINUTE, offset);
                setCalNow(secondArray, Calendar.SECOND, offset);
                setCalNow(fractionArray, "FRACTION", offset);
        
                // From here start to build a new HighPrecisionTimestamp from the specified value
                // To write it back to the table.
        
                // Format fractions to 12 digits with leading zeros
                var df = new DecimalFormat("000000000000");
                var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd-HH.mm.ss");
                var sdfTimezone = new java.text.SimpleDateFormat("Z");
                var timezone = sdfTimezone.format(calNow.getTime());
                timezone = timezone.substring(0,3) + ":" + timezone.substring(3,5);
                if (hpt.isHasTimezone()) {
                    timezone = hpt.getTimeZone();
                }
                resultHcp = new HighPrecisionTimestampFormat((calNow.get(Calendar.YEAR) > 9999 ? "y" : "") + "yyyy-MM-dd-HH.mm.ss", ".SSSSSSSSSSSSZ").parse(sdf.format(calNow.getTime()) + "." + df.format(newFraction) + timezone);
            }
            if (inputIsString) {
                dataArray[columnIndex] = hptFormat.format(resultHcp);
            } else {
                dataArray[columnIndex] = resultHcp;
            }
        }
        
        function checkInput(inputVariable, inputArray) {
            var input = inputVariable;
            var outputArray = inputArray;
        
            if (input == null || input.length <= 0 || input == "null") {
                return outputArray;
            }
        
            if (input.substring(0, 3) == "NOW") {
                outputArray.modify = true;
                outputArray.baseValue = input.substring(0, 3);
                input = input.substring(3);
            }
        
            var direction = input.substring(0, 1);
            if (direction == '+' || direction == '-' || (outputArray.baseValue != "NOW" && direction == 'x')) {
                outputArray.direction = direction;
                outputArray.value = 1;
                input = input.substring(1);
            }
        
            if (input.substring(0, 4) == "SEED" || input.substring(0, 4) == "RAND") {
                outputArray.modify = true;
                outputArray.baseValue = input.substring(0, 4);
                input = input.substring(4);
        
                if (input.substring(0, 1) == "(" && input.substring(input.length - 1, input.length) == ")") {
                    input = input.substring(1, input.length - 1);
                    var words = input.split(":");
                    var min = parseInt(words[0]);
                    var max = parseInt(words[1]);
        
                    if (min != 'NaN' && min > inputArray.min) {
                        outputArray.min = min;
                    }
                    if (max != 'NaN' && max < inputArray.max) {
                        outputArray.max = max;
                    }
        
                    if (outputArray.min > outputArray.max) {
                        var temp = outputArray.min;
                        outputArray.min = outputArray.max;
                        outputArray.max = temp;
                    }
                    input = '';
                }
            }
        
            if (input.length > 0) {
                var value = parseInt(input);
                if (value != 'NaN') {
                    outputArray.modify = true;
                    outputArray.value = value;
                }
            }
        
            return outputArray;
        }
        
        function setCalNow(inputArray, field, offset) {
            if (field == "FRACTION")  {
                newFraction = hpt.getFraction();
                newFraction *= 1000000000000;
                if (inputArray.modify == false) {
                    return;
                }
                var newValue = inputArray.value;
                // Set the base value to be modified depending on the users input
                var addValue = true;
                switch (inputArray.baseValue) {
                    case "NOW":
                        newValue = calNow.get(Calendar.MILLISECOND) * 1000000000;
                        break;
                    case "SEED":
                        var hash = HashUtils.hashStringLikeInt(HashUtils.hashStringLikeInt(seedValue) + offset);
                        newValue = (hash % (inputArray.max - inputArray.min)) + inputArray.min;
                        addValue = (hash % 2) == 0;
                        break;
                    case "RAND":
                        newValue = Math.floor(Math.random() * (inputArray.max - inputArray.min)) + inputArray.min;
                        addValue = (Math.random() > 0.5);
                        break;
                    case "SET":
                        newValue = hpt.getFraction();
                        break;
                    default:
                }
        
                // Modify the base value by setting it to the target or add or subtract a value from it.
                switch (inputArray.direction) {
                    case "x":
                        if (addValue) {
                            newFraction += newValue;
                        } else {
                            newFraction -= newValue;
                        }
                        break;
                    case "+":
                        newFraction += newValue;
                        break;
                    case "-":
                        newFraction -= newValue;
                        break;
                    default:
                        if (newValue < inputArray.min) {
                            newValue = inputArray.min;
                        }
                        if (newValue > inputArray.max) {
                            newValue = inputArray.max;
                        }
                        newFraction = newValue;
                }
        
                while (newFraction >= 1000000000000) {
                    calNow.add(Calendar.SECOND, 1);
                    newFraction -= 1000000000000;
                }
        
                while (newFraction < 0) {
                    calNow.add(Calendar.SECOND, -1);
                    newFraction += 1000000000000;
                }
            } else {
                // if not to be modified copy the value from the input column
                if (inputArray.modify == false) {
                    calNow.set(field, cal.get(field));
                    return;
                }
        
                var newValue = inputArray.value;
                var addValue = true;
                // Set the base value to be modified depending on the users input
                switch (inputArray.baseValue) {
                    case "NOW":
                        // calNow already has the current value
                        break;
                    case "SEED":
                        // generate the new value to add
                        newValue = HashUtils.hashInt(HashUtils.hashStringLikeInt(seedValue) + offset, (inputArray.max - inputArray.min)) + inputArray.min;
                        addValue = (HashUtils.hashInt(HashUtils.hashStringLikeInt(seedValue) + offset, 2) == 1);
                        // set calNow to the input value to use this as base when adding or subtracting the new value
                        calNow.set(field, cal.get(field));
                        break;
                    case "RAND":
                        // generate the new value to add
                        newValue = Math.floor(Math.random() * (inputArray.max - inputArray.min)) + inputArray.min;
                        addValue = (Math.random() > 0.5);
                        // set calNow to the input value to use this as base when adding or subtracting the new value
                        calNow.set(field, cal.get(field));
                        break;
                    default:
                        // set calNow to the input value to use this as base when adding or subtracting the new value
                        calNow.set(field, cal.get(field));
                }
                // Modify the base value by setting it to the target or add or subtract a value from it.
                switch (inputArray.direction) {
                    case "x":
                        if (addValue) {
                            calNow.add(field, newValue);
                        } else {
                            calNow.add(field, newValue * -1);
                        }
                        break;
                    case "+":
                        calNow.add(field, newValue);
                        break;
                    case "-":
                        calNow.add(field, newValue * -1);
                        break;
                    default:
                        if (inputArray.baseValue != "NOW") {
                            // adjust the day of month to a valid day for the given month
                            if (field == Calendar.DAY_OF_MONTH) {
                                var maxdays = calNow.getActualMaximum(Calendar.DAY_OF_MONTH);
                                if (newValue > maxdays) {
                                    newValue = maxdays;
                                }
                            }
                            if (newValue < inputArray.min) {
                                newValue = inputArray.min;
                            }
                            if (newValue > inputArray.max) {
                                newValue = inputArray.max;
                            }
                            // the month value is 0 base in the calender therefore we have to subtract 1 to get the desired month
                            if (field == Calendar.MONTH) {
                                newValue -= 1;
                            }
                            calNow.set(field, newValue);
                        }
                }
            }
        }
        
        /*
         * This function is executed once after the modification of a table is complete. The method
         * is called once per table.
         */
        function close() {
        
        }
    parameters:
    - displayName: Year
      description: |
          The rule or value that will be used to build a new year value for the given time data.
          The possible rules are described in the methods descriptions.
      variableName: year
      dataType: STRING
    - displayName: Month
      description: |
          The rule or value that will be used to build a new month value for the given time data.
          The possible rules are described in the methods descriptions.
      variableName: month
      dataType: STRING
    - displayName: Day
      description: |
          The rule or value that will be used to build a new day value for the given time data.
          The possible rules are described in the methods descriptions.
      variableName: day
      dataType: STRING
    - displayName: Hour
      description: |
          The rule or value that will be used to build a new hour value for the given time data.
          The possible rules are described in the methods descriptions.
      variableName: hour
      dataType: STRING
    - displayName: Minute
      description: |
          The rule or value that will be used to build a new minute value for the given time data.
          The possible rules are described in the methods descriptions.
      variableName: minute
      dataType: STRING
    - displayName: Second
      description: |
          The rule or value that will be used to build a new second value for the given time data.
          The possible rules are described in the methods descriptions.
      variableName: second
      dataType: STRING
    - displayName: Fraction
      description: |
          The rule or value that will be used to build a new fraction value for the given time data.
          The possible rules are described in the methods descriptions.
      variableName: fraction
      dataType: STRING
    - displayName: Seed
      description: |
          Specify the seed which can be used to generate new values for the time data parts.
          If a column name is specified the value of this column will be used as seed.
      variableName: seed
      dataType: STRING
    - displayName: FormatPattern
      description: |
          When the input value is a string this pattern will be used to parse the value of this string into a timestamp and to format the output into the same format.
      variableName: formatPattern
      defaultValue: "yyyy-MM-dd-HH.mm.ss"
      dataType: STRING
    - displayName: FractionPattern
      description: |
          When the input value is a string this pattern will be used to parse the value of this string into the fractions part of the timestamp and to format the output into the same format.
      variableName: fractionPattern
      defaultValue: ".SSSSSSSSSSSSZ"
      dataType: STRING
    - displayName: IgnoreNullInputs
      description: |
          Specify if null value inputs should be ignored. If set to false new values are generated for null inputs.
      variableName: ignoreNull
      defaultValue: "true"
      dataType: BOOLEAN
