summaryrefslogtreecommitdiff
path: root/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/MathUtils.java
blob: 7c803294f4405b9ee68867401a8f9b230777fa38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */
package org.apache.qpid.junit.extensions.util;

import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Mathematical support methods for the toolkit. Caculating averages, variances, min/max for test latencies and
 * generating linear/exponential sequences for test size/concurrency ramping up.
 *
 * <p/>The sequence specifications are of the form [lowest(, ...)(, highest)](,sample=s)(,exp), where round brackets
 * enclose optional values. Using this pattern form it is possible to specify a single value, a range of values divided
 * into s samples, a range of values divided into s samples but distributed exponentially, or a fixed set of samples.
 *
 * <p/>The duration arguments are of the form (dD)(hH)(mM)(sS), where round brackets enclose optional values. At least
 * one of the optional values must be present.
 *
 * <p/><table id="crc"><caption>CRC Card</caption>
 * <tr><th> Responsibilities <th> Collaborations
 * <tr><td> Generate a sequene of integers from a sequence specification.
 * <tr><td> Parse an encoded duration into milliseconds.
 * </table>
 *
 * @author Rupert Smith
 */
public class MathUtils
{
    /** Used for debugging. */
    // private static final Logger log = Logger.getLogger(MathUtils.class);

    /** The sequence defintion matching regular expression. */
    public static final String SEQUENCE_REGEXP = "^(\\[[0-9:]+\\])(:samples=[0-9]+)?(:exp)?$";

    /** The regular expression that matches sequence definitions. */
    private static final Pattern SEQUENCE_PATTERN = Pattern.compile(SEQUENCE_REGEXP);

    /** The duration definition matching regular expression. */
    public static final String DURATION_REGEXP = "^(\\d+D)?(\\d+H)?(\\d+M)?(\\d+S)?$";

    /** The regular expression that matches the duration expression. */
    public static final Pattern DURATION_PATTERN = Pattern.compile(DURATION_REGEXP);

    /** For matching name=value pairs. */
    public static final String NAME_VALUE_REGEXP = "^\\w+=\\w+$";

    /** For matching name=[value1: value2: ...] variations. */
    public static final String NAME_VALUE_VARIATION_REGEXP = "^\\w+=\\[[\\w:]+\\]$";

    /** For matching name=[n: ... :m](:sample=s)(:exp) sequences. */
    public static final String NAME_VALUE_SEQUENCE_REGEXP = "^\\w+=(\\[[0-9:]+\\])(:samples=[0-9]+)?(:exp)?$";

    /** The regular expression that matches name=value pairs and variations. */
    public static final Pattern NAME_VALUE_PATTERN =
        Pattern.compile("(" + NAME_VALUE_REGEXP + ")|(" + NAME_VALUE_VARIATION_REGEXP + ")|(" + NAME_VALUE_SEQUENCE_REGEXP
            + ")");

    /**
     * Runs a quick test of the sequence generation methods to confirm that they work as expected.
     *
     * @param args The command line parameters.
     */
    public static void main(String[] args)
    {
        // Use the command line parser to evaluate the command line.
        CommandLineParser commandLine =
            new CommandLineParser(
                new String[][]
                {
                    { "s", "The sequence definition.", "[m:...:n](:sample=s)(:exp)", "true", MathUtils.SEQUENCE_REGEXP },
                    { "d", "The duration definition.", "dDhHmMsS", "false", MathUtils.DURATION_REGEXP }
                });

        // Capture the command line arguments or display errors and correct usage and then exit.
        ParsedProperties options = null;

        try
        {
            options = new ParsedProperties(commandLine.parseCommandLine(args));
        }
        catch (IllegalArgumentException e)
        {
            System.out.println(commandLine.getErrors());
            System.out.println(commandLine.getUsage());
            System.exit(-1);
        }

        // Extract the command line options.
        String sequence = options.getProperty("s");
        String durationString = options.getProperty("d");

        System.out.println("Sequence is: " + printArray(parseSequence(sequence)));

        if (durationString != null)
        {
            System.out.println("Duration is: " + parseDuration(durationString));
        }
    }

    /**
     * Given a start and end and a number of steps this method generates a sequence of evenly spaced integer
     * values, starting at the start (inclusive) and finishing at the end (inclusive) with the specified number
     * of values in the sequence. The sequence returned may contain less than the specified number where the integer
     * range between start and end is too small to contain that many.
     *
     * <p/>As the results are integers, they will not be perfectly evenly spaced but a best-fit.
     *
     * @param start The sequence start.
     * @param end   The sequence end.
     * @param steps The number of steps.
     *
     * @return The sequence.
     */
    public static int[] generateSequence(int start, int end, int steps)
    {
        // Check that there are at least two steps.
        if (steps < 2)
        {
            throw new IllegalArgumentException("There must be at least 2 steps.");
        }

        ArrayList<Integer> result = new ArrayList<Integer>();

        // Calculate the sequence using floating point, then round into the results.
        double fStart = start;
        double fEnd = end;
        double fCurrent = start;

        for (int i = 0; i < steps; i++)
        {
            fCurrent = (((fEnd - fStart) / (steps - 1)) * i) + fStart;

            roundAndAdd(result, fCurrent);
        }

        // Return the results after converting to a primitive array.
        return intListToPrimitiveArray(result);
    }

    /**
     * Given a start and end and a number of steps this method generates a sequence of expontentially spaced integer
     * values, starting at the start (inclusive) and finishing at the end (inclusive) with the specified number
     * of values in the sequence. An exponentially spaced sequence is one where the ratio between any two consecutive
     * numbers in the sequence remains constant. The sequence returned may contain less than the specified number where
     * the difference between two consecutive values is too small (this is more likely at the start of the sequence,
     * where the values are closer together).
     *
     * <p/>As the results are integers, they will not be perfectly exponentially spaced but a best-fit.
     *
     * @param start The sequence start.
     * @param end   The sequence end.
     * @param steps The number of steps.
     *
     * @return The sequence.
     */
    public static int[] generateExpSequence(int start, int end, int steps)
    {
        // Check that there are at least two steps.
        if (steps < 2)
        {
            throw new IllegalArgumentException("There must be at least 2 steps.");
        }

        ArrayList<Integer> result = new ArrayList<Integer>();

        // Calculate the sequence using floating point, then round into the results.
        double fStart = start;
        double fEnd = end;
        // float fCurrent = start;
        double diff = fEnd - fStart;
        double factor = java.lang.Math.pow(diff, (1.0f / (steps - 1)));

        for (int i = 0; i < steps; i++)
        {
            // This is a cheat to get the end exactly on and lose the accumulated rounding error.
            if (i == (steps - 1))
            {
                result.add(end);
            }
            else
            {
                roundAndAdd(result, fStart - 1.0f + java.lang.Math.pow(factor, i));
            }
        }

        // Return the results after converting to a primitive array.
        return intListToPrimitiveArray(result);
    }

    /**
     * Parses a string defintion of a sequence into an int array containing the sequence. The definition will conform
     * to the regular expression: "^(\[[0-9,]+\])(,samples=[0-9]+)?(,exp)?$". This splits it into three parts,
     * an array of integers, the optional sample count and the optional exponential flag.
     *
     * @param sequenceDef The sequence definition.
     *
     * @return The sequence as a fully expanded int array.
     */
    public static int[] parseSequence(String sequenceDef)
    {
        // Match the sequence definition against the regular expression for sequences.
        Matcher matcher = SEQUENCE_PATTERN.matcher(sequenceDef);

        // Check that the argument is of the right format accepted by this method.
        if (!matcher.matches())
        {
            throw new IllegalArgumentException("The sequence definition is not in the correct format.");
        }

        // Get the total number of matching groups to see if either of the optional samples or exponential flag
        // goups were set.
        int numGroups = matcher.groupCount();

        // Split the array of integers on commas.
        String intArrayString = matcher.group(1);

        String[] intSplits = intArrayString.split("[:\\[\\]]");

        int[] sequence = new int[intSplits.length - 1];

        for (int i = 1; i < intSplits.length; i++)
        {
            sequence[i - 1] = Integer.parseInt(intSplits[i]);
        }

        // Check for the optional samples count.
        int samples = 0;

        if ((numGroups > 1) && (matcher.group(2) != null))
        {
            String samplesGroup = matcher.group(2);

            String samplesString = samplesGroup.substring(",samples=".length());
            samples = Integer.parseInt(samplesString);
        }

        // Check for the optional exponential flag.
        boolean expFlag = false;

        if ((numGroups > 2) && (matcher.group(3) != null))
        {
            expFlag = true;
        }

        // If there is a sample count and 2 or more sequence values defined, then generate the sequence from the first
        // and last sequence values.
        if ((samples != 0) && (sequence.length >= 2))
        {
            int start = sequence[0];
            int end = sequence[sequence.length - 1];

            if (!expFlag)
            {
                sequence = generateSequence(start, end, samples);
            }
            else
            {
                sequence = generateExpSequence(start, end, samples);
            }
        }

        return sequence;
    }

    /**
     * Parses a duration defined as a string, giving a duration in days, hours, minutes and seconds into a number
     * of milliseconds equal to that duration.
     *
     * @param duration The duration definition string.
     *
     * @return The duration in millliseconds.
     */
    public static long parseDuration(String duration)
    {
        // Match the duration against the regular expression.
        Matcher matcher = DURATION_PATTERN.matcher(duration);

        // Check that the argument is of the right format accepted by this method.
        if (!matcher.matches())
        {
            throw new IllegalArgumentException("The duration definition is not in the correct format.");
        }

        // This accumulates the duration.
        long result = 0;

        int numGroups = matcher.groupCount();

        // Extract the days.
        if (numGroups >= 1)
        {
            String daysString = matcher.group(1);
            result +=
                (daysString == null)
                ? 0 : (Long.parseLong(daysString.substring(0, daysString.length() - 1)) * 24 * 60 * 60 * 1000);
        }

        // Extract the hours.
        if (numGroups >= 2)
        {
            String hoursString = matcher.group(2);
            result +=
                (hoursString == null) ? 0
                                      : (Long.parseLong(hoursString.substring(0, hoursString.length() - 1)) * 60 * 60 * 1000);
        }

        // Extract the minutes.
        if (numGroups >= 3)
        {
            String minutesString = matcher.group(3);
            result +=
                (minutesString == null)
                ? 0 : (Long.parseLong(minutesString.substring(0, minutesString.length() - 1)) * 60 * 1000);
        }

        // Extract the seconds.
        if (numGroups >= 4)
        {
            String secondsString = matcher.group(4);
            result +=
                (secondsString == null) ? 0 : (Long.parseLong(secondsString.substring(0, secondsString.length() - 1)) * 1000);
        }

        return result;
    }

    /**
     * Pretty prints an array of ints as a string.
     *
     * @param array The array to pretty print.
     *
     * @return The pretty printed string.
     */
    public static String printArray(int[] array)
    {
        String result = "[";
        for (int i = 0; i < array.length; i++)
        {
            result += array[i];
            result += (i < (array.length - 1)) ? ", " : "";
        }

        result += "]";

        return result;
    }

    /**
     * Returns the maximum value in an array of integers.
     *
     * @param values The array to find the amx in.
     *
     * @return The max value.
     */
    public static int maxInArray(int[] values)
    {
        if ((values == null) || (values.length == 0))
        {
            throw new IllegalArgumentException("Cannot find the max of a null or empty array.");
        }

        int max = values[0];

        for (int value : values)
        {
            max = (max < value) ? value : max;
        }

        return max;
    }

    /**
     * The #toArray methods of collections cannot be used with primitive arrays. This loops over and array list
     * of Integers and outputs and array of int.
     *
     * @param result The array of Integers to convert.
     *
     * @return An array of int.
     */
    private static int[] intListToPrimitiveArray(ArrayList<Integer> result)
    {
        int[] resultArray = new int[result.size()];
        int index = 0;
        for (int r : result)
        {
            resultArray[index] = result.get(index);
            index++;
        }

        return resultArray;
    }

    /**
     * Rounds the specified floating point value to the nearest integer and adds it to the specified list of
     * integers, provided it is not already in the list.
     *
     * @param result The list of integers to add to.
     * @param value  The new candidate to round and add to the list.
     */
    private static void roundAndAdd(ArrayList<Integer> result, double value)
    {
        int roundedValue = (int) Math.round(value);

        if (!result.contains(roundedValue))
        {
            result.add(roundedValue);
        }
    }
}