Merge function
This commit is contained in:
		
							parent
							
								
									6265ff45a8
								
							
						
					
					
						commit
						933d77427c
					
				@ -89,7 +89,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
 | 
			
		||||
        for (var e : tsRecords.entrySet()) {
 | 
			
		||||
            values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
 | 
			
		||||
        }
 | 
			
		||||
        return new TbelCfTsRollingArg(limit, timeWindow, values);
 | 
			
		||||
        return new TbelCfTsRollingArg(timeWindow, values);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,6 @@ public class TbTimeWindow implements TbelCfObject {
 | 
			
		||||
 | 
			
		||||
    private long startTs;
 | 
			
		||||
    private long endTs;
 | 
			
		||||
    private int limit;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public long memorySize() {
 | 
			
		||||
 | 
			
		||||
@ -368,16 +368,6 @@ public class TbUtils {
 | 
			
		||||
                byte[].class, int.class)));
 | 
			
		||||
        parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt",
 | 
			
		||||
                byte[].class, int.class, int.class)));
 | 
			
		||||
        parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge",
 | 
			
		||||
                TbelCfTsRollingArg.class, TbelCfTsRollingArg.class)));
 | 
			
		||||
        parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge",
 | 
			
		||||
                TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class)));
 | 
			
		||||
        parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge",
 | 
			
		||||
                TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class, Map.class)));
 | 
			
		||||
        parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge",
 | 
			
		||||
                List.class, TbTimeWindow.class)));
 | 
			
		||||
        parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge",
 | 
			
		||||
                List.class, TbTimeWindow.class, Map.class)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String btoa(String input) {
 | 
			
		||||
@ -1518,76 +1508,5 @@ public class TbUtils {
 | 
			
		||||
        return hex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b) {
 | 
			
		||||
        return merge(Arrays.asList(a, b), null, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow) {
 | 
			
		||||
        return merge(Arrays.asList(a, b), timeWindow, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow, Map<String, Object> settings) {
 | 
			
		||||
        return merge(Arrays.asList(a, b), timeWindow, settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TbelCfTsRollingData merge(List<TbelCfTsRollingArg> args, TbTimeWindow timeWindow) {
 | 
			
		||||
        return merge(args, timeWindow, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TbelCfTsRollingData merge(List<TbelCfTsRollingArg> args, TbTimeWindow timeWindow, Map<String, Object> settings) {
 | 
			
		||||
        boolean ignoreNaN = true;
 | 
			
		||||
        if (settings != null && settings.containsKey("ignoreNaN")) {
 | 
			
		||||
            ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        TreeSet<Long> allTimestamps = new TreeSet<>();
 | 
			
		||||
        long startTs = Long.MAX_VALUE;
 | 
			
		||||
        long endTs = Long.MIN_VALUE;
 | 
			
		||||
        for (TbelCfTsRollingArg arg : args) {
 | 
			
		||||
            for (TbelCfTsDoubleVal val : arg.getValues()) {
 | 
			
		||||
                allTimestamps.add(val.getTs());
 | 
			
		||||
            }
 | 
			
		||||
            startTs = Math.min(startTs, arg.getTimeWindow().getStartTs());
 | 
			
		||||
            endTs = Math.max(endTs, arg.getTimeWindow().getEndTs());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<TbelCfTsMultiDoubleVal> data = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        int[] lastIndex = new int[args.size()];
 | 
			
		||||
        double[] result = new double[args.size()];
 | 
			
		||||
        Arrays.fill(result, Double.NaN);
 | 
			
		||||
 | 
			
		||||
        var tw = timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs, allTimestamps.size());
 | 
			
		||||
 | 
			
		||||
        for (long ts : allTimestamps) {
 | 
			
		||||
            for (int i = 0; i < args.size(); i++) {
 | 
			
		||||
                var arg = args.get(i);
 | 
			
		||||
                var values = arg.getValues();
 | 
			
		||||
                while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) {
 | 
			
		||||
                    result[i] = values.get(lastIndex[i]).getValue();
 | 
			
		||||
                    lastIndex[i]++;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (tw.matches(ts)) {
 | 
			
		||||
                if (ignoreNaN) {
 | 
			
		||||
                    boolean skip = false;
 | 
			
		||||
                    for (int i = 0; i < args.size(); i++) {
 | 
			
		||||
                        if (Double.isNaN(result[i])) {
 | 
			
		||||
                            skip = true;
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (!skip) {
 | 
			
		||||
                        data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new TbelCfTsRollingData(tw, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,11 +19,15 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonIgnore;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.TreeSet;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
 | 
			
		||||
@ -44,9 +48,9 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
 | 
			
		||||
        this.values = Collections.unmodifiableList(values);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TbelCfTsRollingArg(int limit, long timeWindow, List<TbelCfTsDoubleVal> values) {
 | 
			
		||||
    public TbelCfTsRollingArg(long timeWindow, List<TbelCfTsDoubleVal> values) {
 | 
			
		||||
        long ts = System.currentTimeMillis();
 | 
			
		||||
        this.timeWindow = new TbTimeWindow(ts - timeWindow, ts, limit);
 | 
			
		||||
        this.timeWindow = new TbTimeWindow(ts - timeWindow, ts);
 | 
			
		||||
        this.values = Collections.unmodifiableList(values);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -104,6 +108,14 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
 | 
			
		||||
        return min;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public double avg() {
 | 
			
		||||
        return avg(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public double avg(boolean ignoreNaN) {
 | 
			
		||||
        return mean(ignoreNaN);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public double mean() {
 | 
			
		||||
        return mean(true);
 | 
			
		||||
    }
 | 
			
		||||
@ -256,6 +268,88 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
 | 
			
		||||
        return sum;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TbelCfTsRollingData merge(TbelCfTsRollingArg other) {
 | 
			
		||||
        return mergeAll(Collections.singletonList(other), null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TbelCfTsRollingData merge(TbelCfTsRollingArg other, Map<String, Object> settings) {
 | 
			
		||||
        return mergeAll(Collections.singletonList(other), settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others) {
 | 
			
		||||
        return mergeAll(others, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others, Map<String, Object> settings) {
 | 
			
		||||
        List<TbelCfTsRollingArg> args = new ArrayList<>(others.size() + 1);
 | 
			
		||||
        args.add(this);
 | 
			
		||||
        args.addAll(others);
 | 
			
		||||
 | 
			
		||||
        boolean ignoreNaN = true;
 | 
			
		||||
        if (settings != null && settings.containsKey("ignoreNaN")) {
 | 
			
		||||
            ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        TbTimeWindow timeWindow = null;
 | 
			
		||||
        if (settings != null && settings.containsKey("timeWindow")) {
 | 
			
		||||
            var twVar = settings.get("timeWindow");
 | 
			
		||||
            if (twVar instanceof TbTimeWindow) {
 | 
			
		||||
                timeWindow = (TbTimeWindow) settings.get("timeWindow");
 | 
			
		||||
            } else if (twVar instanceof Map twMap) {
 | 
			
		||||
                timeWindow = new TbTimeWindow(Long.valueOf(twMap.get("startTs").toString()), Long.valueOf(twMap.get("endTs").toString()));
 | 
			
		||||
            } else {
 | 
			
		||||
                timeWindow = JacksonUtil.fromString(settings.get("timeWindow").toString(), TbTimeWindow.class);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        TreeSet<Long> allTimestamps = new TreeSet<>();
 | 
			
		||||
        long startTs = Long.MAX_VALUE;
 | 
			
		||||
        long endTs = Long.MIN_VALUE;
 | 
			
		||||
        for (TbelCfTsRollingArg arg : args) {
 | 
			
		||||
            for (TbelCfTsDoubleVal val : arg.getValues()) {
 | 
			
		||||
                allTimestamps.add(val.getTs());
 | 
			
		||||
            }
 | 
			
		||||
            startTs = Math.min(startTs, arg.getTimeWindow().getStartTs());
 | 
			
		||||
            endTs = Math.max(endTs, arg.getTimeWindow().getEndTs());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<TbelCfTsMultiDoubleVal> data = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        int[] lastIndex = new int[args.size()];
 | 
			
		||||
        double[] result = new double[args.size()];
 | 
			
		||||
        Arrays.fill(result, Double.NaN);
 | 
			
		||||
 | 
			
		||||
        for (long ts : allTimestamps) {
 | 
			
		||||
            for (int i = 0; i < args.size(); i++) {
 | 
			
		||||
                var arg = args.get(i);
 | 
			
		||||
                var values = arg.getValues();
 | 
			
		||||
                while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) {
 | 
			
		||||
                    result[i] = values.get(lastIndex[i]).getValue();
 | 
			
		||||
                    lastIndex[i]++;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (timeWindow == null || timeWindow.matches(ts)) {
 | 
			
		||||
                if (ignoreNaN) {
 | 
			
		||||
                    boolean skip = false;
 | 
			
		||||
                    for (int i = 0; i < args.size(); i++) {
 | 
			
		||||
                        if (Double.isNaN(result[i])) {
 | 
			
		||||
                            skip = true;
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (!skip) {
 | 
			
		||||
                        data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new TbelCfTsRollingData(timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs), data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @JsonIgnore
 | 
			
		||||
    public int getSize() {
 | 
			
		||||
        return values.size();
 | 
			
		||||
 | 
			
		||||
@ -1137,75 +1137,6 @@ public class TbUtilsTest {
 | 
			
		||||
        Assertions.assertEquals(expected, actual);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_ts_match_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = TbUtils.merge(arg1, arg2);
 | 
			
		||||
        Assertions.assertEquals(3, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues().get(0));
 | 
			
		||||
        Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
 | 
			
		||||
        Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_with_timewindow_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = TbUtils.merge(arg1, arg2, new TbTimeWindow(0, 10000, 1000));
 | 
			
		||||
        Assertions.assertEquals(2, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues().get(0));
 | 
			
		||||
        Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
 | 
			
		||||
        Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_ts_mismatch_default_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = TbUtils.merge(arg1, arg2);
 | 
			
		||||
        Assertions.assertEquals(3, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
 | 
			
		||||
        TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
 | 
			
		||||
        Assertions.assertNotNull(item0);
 | 
			
		||||
        Assertions.assertEquals(200L, item0.getTs());
 | 
			
		||||
        Assertions.assertEquals(1, item0.getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, item0.getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = TbUtils.merge(Arrays.asList(arg1, arg2), new TbTimeWindow(0, 60000, 1000), Collections.singletonMap("ignoreNaN", false));
 | 
			
		||||
        Assertions.assertEquals(4, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
 | 
			
		||||
        TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
 | 
			
		||||
        Assertions.assertNotNull(item0);
 | 
			
		||||
        Assertions.assertEquals(100L, item0.getTs());
 | 
			
		||||
        Assertions.assertEquals(1, item0.getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(Double.NaN, item0.getValues()[1]);
 | 
			
		||||
 | 
			
		||||
        TbelCfTsMultiDoubleVal item1 = result.getValues().get(1);
 | 
			
		||||
        Assertions.assertEquals(200L, item1.getTs());
 | 
			
		||||
        Assertions.assertEquals(1, item1.getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, item1.getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static List<Byte> toList(byte[] data) {
 | 
			
		||||
        List<Byte> result = new ArrayList<>(data.length);
 | 
			
		||||
        for (Byte b : data) {
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,15 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.script.api.tbel;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Assertions;
 | 
			
		||||
import org.junit.jupiter.api.BeforeEach;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 | 
			
		||||
@ -33,7 +38,7 @@ public class TbelCfTsRollingArgTest {
 | 
			
		||||
    @BeforeEach
 | 
			
		||||
    void setUp() {
 | 
			
		||||
        rollingArg = new TbelCfTsRollingArg(
 | 
			
		||||
                new TbTimeWindow(ts - 30000, ts - 10, 10),
 | 
			
		||||
                new TbTimeWindow(ts - 30000, ts - 10),
 | 
			
		||||
                List.of(
 | 
			
		||||
                        new TbelCfTsDoubleVal(ts - 10, Double.NaN),
 | 
			
		||||
                        new TbelCfTsDoubleVal(ts - 20, 2.0),
 | 
			
		||||
@ -98,7 +103,7 @@ public class TbelCfTsRollingArgTest {
 | 
			
		||||
    void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() {
 | 
			
		||||
        assertThat(rollingArg.first()).isEqualTo(2.0);
 | 
			
		||||
        rollingArg = new TbelCfTsRollingArg(
 | 
			
		||||
                new TbTimeWindow(ts - 30000, ts - 10, 10),
 | 
			
		||||
                new TbTimeWindow(ts - 30000, ts - 10),
 | 
			
		||||
                List.of(
 | 
			
		||||
                        new TbelCfTsDoubleVal(ts - 10, Double.NaN),
 | 
			
		||||
                        new TbelCfTsDoubleVal(ts - 40, Double.NaN),
 | 
			
		||||
@ -117,7 +122,7 @@ public class TbelCfTsRollingArgTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void testEmptyValues() {
 | 
			
		||||
        rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10, 10), List.of());
 | 
			
		||||
        rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10), List.of());
 | 
			
		||||
        assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
 | 
			
		||||
        assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
 | 
			
		||||
        assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
 | 
			
		||||
@ -128,4 +133,81 @@ public class TbelCfTsRollingArgTest {
 | 
			
		||||
        assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_ts_match_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = arg1.merge(arg2);
 | 
			
		||||
        Assertions.assertEquals(3, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues().get(0));
 | 
			
		||||
        Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
 | 
			
		||||
        Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_with_timewindow_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = arg1.merge(arg2, Collections.singletonMap("timeWindow", new TbTimeWindow(0, 10000)));
 | 
			
		||||
        Assertions.assertEquals(2, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues().get(0));
 | 
			
		||||
        Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
 | 
			
		||||
        Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
 | 
			
		||||
 | 
			
		||||
        result = arg1.merge(arg2, Collections.singletonMap("timeWindow", Map.of("startTs", 0L, "endTs", 10000)));
 | 
			
		||||
        Assertions.assertEquals(2, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues().get(0));
 | 
			
		||||
        Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
 | 
			
		||||
        Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_ts_mismatch_default_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = arg1.merge(arg2);
 | 
			
		||||
        Assertions.assertEquals(3, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
 | 
			
		||||
        TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
 | 
			
		||||
        Assertions.assertNotNull(item0);
 | 
			
		||||
        Assertions.assertEquals(200L, item0.getTs());
 | 
			
		||||
        Assertions.assertEquals(1, item0.getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, item0.getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() {
 | 
			
		||||
        TbTimeWindow tw = new TbTimeWindow(0, 60000);
 | 
			
		||||
        TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
 | 
			
		||||
        TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
 | 
			
		||||
 | 
			
		||||
        var result = arg1.merge(arg2, Collections.singletonMap("ignoreNaN", false));
 | 
			
		||||
        Assertions.assertEquals(4, result.getSize());
 | 
			
		||||
        Assertions.assertNotNull(result.getValues());
 | 
			
		||||
 | 
			
		||||
        TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
 | 
			
		||||
        Assertions.assertNotNull(item0);
 | 
			
		||||
        Assertions.assertEquals(100L, item0.getTs());
 | 
			
		||||
        Assertions.assertEquals(1, item0.getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(Double.NaN, item0.getValues()[1]);
 | 
			
		||||
 | 
			
		||||
        TbelCfTsMultiDoubleVal item1 = result.getValues().get(1);
 | 
			
		||||
        Assertions.assertEquals(200L, item1.getTs());
 | 
			
		||||
        Assertions.assertEquals(1, item1.getValues()[0]);
 | 
			
		||||
        Assertions.assertEquals(11, item1.getValues()[1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -272,7 +272,7 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = {
 | 
			
		||||
export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  max: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Computes the maximum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    description: 'Returns the maximum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -288,7 +288,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  },
 | 
			
		||||
  min: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Computes the minimum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    description: 'Returns the minimum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -304,7 +304,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  },
 | 
			
		||||
  mean: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Computes the mean value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    description: 'Computes the mean value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -318,9 +318,25 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
      type: 'number'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  avg: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Computes the average value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
        description: 'Whether to ignore NaN values. Equals true by default.',
 | 
			
		||||
        type: 'boolean',
 | 
			
		||||
        optional: true,
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    return: {
 | 
			
		||||
      description: 'The average value, or NaN if applicable',
 | 
			
		||||
      type: 'number'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  std: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Computes the standard deviation in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    description: 'Computes the standard deviation of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -336,7 +352,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  },
 | 
			
		||||
  median: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Computes the median value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    description: 'Computes the median value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -352,7 +368,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  },
 | 
			
		||||
  count: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Counts values in the list of rolling argument values. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
 | 
			
		||||
    description: 'Counts values of the rolling argument. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -368,7 +384,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  },
 | 
			
		||||
  last: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Returns the last non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the last value.',
 | 
			
		||||
    description: 'Returns the last non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the last value.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -384,7 +400,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  },
 | 
			
		||||
  first: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Returns the first non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the first value.',
 | 
			
		||||
    description: 'Returns the first non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the first value.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -400,7 +416,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
  },
 | 
			
		||||
  sum: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Computes the sum of values in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    description: 'Computes the sum of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'ignoreNaN',
 | 
			
		||||
@ -413,12 +429,56 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
 | 
			
		||||
      description: 'The sum of values, or NaN if applicable',
 | 
			
		||||
      type: 'number'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  merge: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Merges current object with other time series rolling argument into a single object by aligning their timestamped values. Supports optional configurable settings.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'other',
 | 
			
		||||
        description: "A time series rolling argument to be merged with the current object.",
 | 
			
		||||
        type: "object",
 | 
			
		||||
        optional: true
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "settings",
 | 
			
		||||
        description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
 | 
			
		||||
        type: "object",
 | 
			
		||||
        optional: true
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    return: {
 | 
			
		||||
      description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
 | 
			
		||||
      type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mergeAll: {
 | 
			
		||||
    meta: 'function',
 | 
			
		||||
    description: 'Merges current object with other time series rolling arguments into a single object by aligning their timestamped values. Supports optional configurable settings.',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'others',
 | 
			
		||||
        description: "A list of time series rolling arguments to be merged with the current object.",
 | 
			
		||||
        type: "object[]",
 | 
			
		||||
        optional: true
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "settings",
 | 
			
		||||
        description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
 | 
			
		||||
        type: "object",
 | 
			
		||||
        optional: true
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    return: {
 | 
			
		||||
      description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
 | 
			
		||||
      type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const CalculatedFieldRollingValueArgumentAutocomplete = {
 | 
			
		||||
  meta: 'object',
 | 
			
		||||
  type: '{ values: { ts: number; value: any; }[]; timeWindow: { startTs: number; endTs: number; limit: number } }; }',
 | 
			
		||||
  type: '{ values: { ts: number; value: number; }[]; timeWindow: { startTs: number; endTs: number } }; }',
 | 
			
		||||
  description: 'Calculated field rolling value argument.',
 | 
			
		||||
  children: {
 | 
			
		||||
    ...CalculatedFieldRollingValueArgumentFunctionsAutocomplete,
 | 
			
		||||
@ -429,7 +489,7 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
 | 
			
		||||
    },
 | 
			
		||||
    timeWindow: {
 | 
			
		||||
      meta: 'object',
 | 
			
		||||
      type: '{ startTs: number; endTs: number; limit: number }',
 | 
			
		||||
      type: '{ startTs: number; endTs: number }',
 | 
			
		||||
      description: 'Time window configuration',
 | 
			
		||||
      children: {
 | 
			
		||||
        startTs: {
 | 
			
		||||
@ -441,11 +501,6 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
 | 
			
		||||
          meta: 'number',
 | 
			
		||||
          type: 'number',
 | 
			
		||||
          description: 'End time stamp',
 | 
			
		||||
        },
 | 
			
		||||
        limit: {
 | 
			
		||||
          meta: 'number',
 | 
			
		||||
          type: 'number',
 | 
			
		||||
          description: 'Limit',
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -504,7 +559,7 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array<AceHighlightRule> =
 | 
			
		||||
  ['max', 'min', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum'].map(funcName => ({
 | 
			
		||||
  ['max', 'min', 'avg', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum', 'merge', 'mergeAll'].map(funcName => ({
 | 
			
		||||
    token: 'tb.calculated-field-func',
 | 
			
		||||
    regex: `\\b${funcName}\\b`,
 | 
			
		||||
    next: 'no_regex'
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user