001/*-
002 *******************************************************************************
003 * Copyright (c) 2011, 2016 Diamond Light Source Ltd.
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *    Peter Chang - initial API and implementation and/or initial documentation
011 *******************************************************************************/
012
013package org.eclipse.january.dataset;
014
015import java.io.Serializable;
016
017/**     
018 * The {@code Slice} class represents the set of indices (start, stop, step), that are used to extract specifics subsets of {@link org.eclipse.january.dataset.Dataset}.<br><br>
019 * The start argument default to 0, stop argument default to the stop argument default to the end of the dimension that the slice is applied to, and the default argument for the step is 1.
020 * <br><br>
021 * The start index is inclusive, for example, if we want to get data from index 1, so sliceData will be <b>[2,3]</b> :
022 * <pre>
023* {@code
024* final Dataset onedData = DatasetFactory.createFromObject(new int[]{1,2,3});
025* Dataset sliceData = onedData.getSlice(new Slice(1, null, null));
026* }
027* </pre>
028* 
029* If Slice is specified with only one argument, this will be the stop index which is exclusive. In this case sliceData will be <b>[1,2]</b> :
030 * <pre>
031* {@code
032* final Dataset onedData = DatasetFactory.createFromObject(new int[]{1,2,3});
033* Dataset sliceData = onedData.getSlice(new Slice(2));
034* }
035* </pre>
036* 
037* To create a 1D Slice, so sliceData is : <b>[6, 5, 4]</b>, we will do :
038* <pre>
039* {@code
040* final Dataset sliceData = DatasetFactory.createFromObject(new int[]{10,9,8,7,6,5,4,3,2,1,0});
041* Dataset newOnedData = sliceData.getSlice(new Slice(4, 7, 1));
042* }
043* </pre>
044* <br>
045* For more informations, see the sliceFrom1D example in SlicingExamples.
046 */
047public class Slice implements Cloneable, Serializable {
048
049        private static final long serialVersionUID = 3714928852236201310L;
050        private Integer start;
051        private Integer stop;
052        private int step;
053
054        private int length; // max length of dimension
055
056        /**
057         * Constructs a Slice object with the start and the stop value representing
058         * the entirety of the sliced dimension of the Dataset.
059         */
060        public Slice() {
061                this(null, null, 1);
062        }
063
064        /**
065         * Constructs a Slice object with, by default the start set to 0 and with a
066         * step of 1. If the stop point of the Slice is {@code null}, it will be set
067         * to the stop argument default to the end of the dimension that the slice
068         * is applied to.
069         * 
070         * @param stop
071         *            the stop point of the Slice
072         */
073        public Slice(final Integer stop) {
074                this(null, stop, 1);
075        }
076
077        /**
078         * Constructs a Slice object with, by default a step of 1. If the start
079         * point of the Slice is {@code null}, it will be set automatically to 0. If
080         * the stop point of the Slice is {@code null}, it will be set to the stop
081         * argument default to the end of the dimension that the slice is applied
082         * to.
083         * 
084         * @param start
085         *            the start point of the Slice
086         * @param stop
087         *            the stop point of the Slice
088         */
089        public Slice(final Integer start, final Integer stop) {
090                this(start, stop, 1);
091        }
092
093        /**
094         * Constructs a Slice object on which it is possible to chooe the start, the
095         * stop and the step. If the start point of the Slice is {@code null}, it
096         * will be set automatically to 0. If the stop point of the Slice is
097         * {@code null}, it will be set to the stop argument default to the end of
098         * the dimension that the slice is applied to. If the the wanted step is set
099         * to {@code null}, it will be set by default to 1.
100         * 
101         * @param start
102         *            the start point of the Slice, may be {@code null}
103         * @param stop
104         *            the stop point of the Slice, may be {@code null}
105         * @param step
106         *            the step wanted to browse the Dataset, may be {@code null}
107         */
108        public Slice(final Integer start, final Integer stop, final Integer step) {
109                this.start = start;
110                this.stop = stop;
111                this.step = step == null ? 1 : step;
112                length = -1;
113        }
114
115        /**
116         * Copy another slice
117         * 
118         * @param other
119         */
120        private Slice(final Slice other) {
121                start = other.start;
122                stop = other.stop;
123                step = other.step;
124                length = other.length;
125        }
126
127        /**
128         * Creates a deep copy of the Slice.
129         * 
130         * @return New Slice with the current Slice properties
131         */
132        @Override
133        public Slice clone() {
134                return new Slice(this);
135        }
136
137        /**
138         * Sets the maximal dimensions length of the Slice.
139         * 
140         * @param length
141         *            Wanted size of dimensions
142         * @return The Slice which the method is called on
143         */
144        public Slice setLength(int length) {
145                if (stop != null) {
146                        if (step > 0) {
147                                if (stop > length) {
148                                        throw new IllegalArgumentException("Stop must be less than or equal to length");
149                                }
150                        }
151                }
152                if (start != null) {
153                        if (start > length) {
154                                throw new IllegalArgumentException("Start must be less than or equal to length");
155                        }
156                }
157                this.length = length;
158                return this;
159        }
160
161        /**
162         * Returns {@code true} if the slice has a maximum size equal to the current
163         * size, else {@code false}.
164         * 
165         * @return {@code true} if slice represents complete dimension,
166         *         {@code false} in the other case.
167         */
168        public boolean isSliceComplete() {
169                if (start == null && stop == null && (step == 1 || step == -1))
170                        return true;
171                if (length > 0) {
172                        return getNumSteps() == length;
173                }
174
175                return true;
176        }
177
178        /**
179         * Returns the maximum value of the slice.
180         * 
181         * @return Maximum value of the slice
182         */
183        public int getLength() {
184                return length;
185        }
186
187        /**
188         * Returns the starting index of the slice.
189         * 
190         * @return Start point of the slice
191         */
192        public Integer getStart() {
193                return start;
194        }
195
196        /**
197         * Returns the stopping index of the slice.
198         * 
199         * @return Stop point of the slice
200         */
201        public Integer getStop() {
202                return stop;
203        }
204
205        /**
206         * Returns the step of the slice.
207         * 
208         * @return Step of the slice
209         */
210        public int getStep() {
211                return step;
212        }
213
214        /**
215         * Set the starting index of the slice. If the start point of the Slice is
216         * {@code null}, it will be set automatically to 0.
217         * 
218         * @param start
219         *            Starting index of the Slice, may be {@code null}
220         */
221        public void setStart(Integer start) {
222                if (start != null && length > 0) {
223                        if (step > 0) {
224                                int end = stop == null ? length : stop;
225                                if (start >= end) {
226                                        throw new IllegalArgumentException("Non-null start must be less than end");
227                                }
228                        } else {
229                                int end = stop == null ? -1 : stop;
230                                if (start < end) {
231                                        throw new IllegalArgumentException("Non-null start must be greater than end for negative step");
232                                }
233                        }
234                }
235                this.start = start;
236        }
237
238        /**
239         * Set the stopping index of the slice. If the stop point of the Slice is
240         * {@code null}, it will be set to the stop argument default to the end of
241         * the dimension that the slice is applied to.
242         * 
243         * @param stop
244         *            Stopping index of the Slice, may be {@code null}
245         */
246        public void setStop(Integer stop) {
247                if (stop != null && length > 0) {
248                        if (step > 0) {
249                                int beg = start == null ? 0 : start;
250                                if (stop < beg) {
251                                        throw new IllegalArgumentException("Non-null stop must be greater than or equal to beginning");
252                                }
253                        } else {
254                                int beg = start == null ? length - 1 : start;
255                                if (stop >= beg) {
256                                        throw new IllegalArgumentException("Non-null stop must be less than beginning for negative step");
257                                }
258                        }
259                        if (stop > length)
260                                stop = length;
261                }
262                this.stop = stop;
263        }
264
265        /**
266         * Move the start and end to an other index keeping the same step and the
267         * same gap between the two values
268         * 
269         * @param beg
270         *            New starting point
271         * @return Return {@code true} if the end was reached, {@code false} in the
272         *         other case.
273         */
274        public boolean setPosition(int beg) {
275                boolean end = false;
276                int len = getNumSteps();
277                int max = getNumSteps(beg, length, step);
278                if (len > max) {
279                        len = max;
280                        end = true;
281                }
282                start = beg;
283                stop = start + (len - 1) * step + 1;
284                return end;
285        }
286
287        /**
288         * Returns the index of the n-th step inside of the slice
289         * 
290         * @param n
291         *            Wanted step index in the slice
292         * @return Return the index of the step inside of the Slice
293         */
294        public int getPosition(int n) {
295                if (n < 0)
296                        throw new IllegalArgumentException("Given n-th step should be non-negative");
297                if (n >= getNumSteps())
298                        throw new IllegalArgumentException("N-th step exceeds extent of slice");
299                int beg;
300                if (start == null) {
301                        if (step < 0) {
302                                if (length < 0) {
303                                        if (stop == null) {
304                                                throw new IllegalStateException("Length or stop should be set");
305                                        }
306                                        beg = stop - 1;
307                                } else {
308                                        beg = length - 1;
309                                }
310                        } else {
311                                beg = 0;
312                        }
313                } else {
314                        beg = start;
315                }
316                return beg + step * n;
317        }
318
319        /**
320         * Set the step size inside of the Slice. If the the wanted step is set to
321         * {@code null}, it will be set by default to 1.
322         * 
323         * @param step
324         *            New wanted step, may be {@code null}
325         */
326        public void setStep(int step) {
327                if (step == 0) {
328                        throw new IllegalArgumentException("Step must not be zero");
329                }
330                this.step = step;
331        }
332
333        /**
334         * Appends a String representation of the Slice comparable to the python
335         * representation.
336         * 
337         * @param s
338         *            String builder
339         * @param len
340         *            Maximal length of the Slice, or -1 if not set
341         * @param beg
342         *            Start index of the Slice
343         * @param end
344         *            Stop index of the Slice
345         * @param del
346         *            Step of the Slice
347         */
348        public static void appendSliceToString(final StringBuilder s, final int len, final int beg, final int end,
349                        final int del) {
350                int o = s.length();
351                if (del > 0) {
352                        if (beg != 0)
353                                s.append(beg);
354                } else {
355                        if (beg != len - 1)
356                                s.append(beg);
357                }
358
359                int n = getNumSteps(beg, end, del);
360                if (n == 1) {
361                        if (s.length() == o) {
362                                s.append(beg);
363                        }
364                        return;
365                }
366
367                s.append(':');
368
369                if (del > 0) {
370                        if (end != len)
371                                s.append(end);
372                } else {
373                        if (end != -1)
374                                s.append(end);
375                }
376
377                if (del != 1) {
378                        s.append(':');
379                        s.append(del);
380                }
381        }
382
383        /**
384         * Returns a string construction of the slice with the python form.
385         * 
386         * @return Constructed String.
387         */
388        @Override
389        public String toString() {
390                StringBuilder s = new StringBuilder();
391                appendSliceToString(s, length, start != null ? start : (step > 0 ? 0 : length - 1),
392                                stop != null ? stop : (step > 0 ? length : -1), step);
393                return s.toString();
394        }
395
396        /**
397         * Returns the number of steps inside of the Slice
398         * 
399         * @return Number of steps inside of the Slice
400         */
401        public int getNumSteps() {
402                if (length < 0) {
403                        if (stop == null)
404                                throw new IllegalStateException("Slice is underspecified - stop is null and length is negative");
405                        int beg = start == null ? (step > 0 ? 0 : stop - 1) : start;
406                        if (step > 0 && stop <= beg)
407                                return 0;
408                        if (step < 0 && stop > beg)
409                                return 0;
410                        return getNumSteps(beg, stop, step);
411                }
412                int beg = start == null ? (step > 0 ? 0 : length - 1) : start;
413                int end = stop == null ? (step > 0 ? length : -1) : stop;
414                return getNumSteps(beg, end, step);
415        }
416
417        /**
418         * Returns the number of steps inside of the Slice from a point to an other
419         * point minus 1 because this is an exclusive index
420         * 
421         * @param beg
422         *            Starting point
423         * @param end
424         *            (exclusive) Stopping point
425         * @return Numbers of steps between the 2 limits.
426         */
427        public int getNumSteps(int beg, int end) {
428                return getNumSteps(beg, end, step);
429        }
430
431        private static int getNumSteps(int beg, int end, int step) {
432                int del = step > 0 ? 1 : -1;
433                return Math.max(0, (end - beg - del) / step + 1);
434        }
435
436        /**
437         * Returns the last value inside of Slice.
438         * 
439         * @return Last value in the slice, it can be a lower value than the start
440         *         if the step is going backward
441         */
442        public int getEnd() {
443                int n = getNumSteps() - 1;
444                if (n < 0)
445                        throw new IllegalStateException("End is not defined");
446
447                return getPosition(n);
448        }
449
450        /**
451         * Flips the Slice direction, after this operation, the slice begins at
452         * previous end point, steps in the opposite direction, and finishes at the
453         * previous start point.
454         * <p>
455         * Note : the stop value may not be preserved across two flips
456         * </p>
457         * 
458         * @return Flipped Slice.
459         */
460        public Slice flip() {
461                if (length < 0) {
462                        Integer tmp = start == null ? null : start - step;
463                        start = stop == null ? null : getEnd();
464                        stop = tmp;
465                } else {
466                        Integer tstart = start;
467                        start = stop == null ? null : getEnd();
468                        stop = tstart == null ? null : tstart - step;
469                }
470                step = -step;
471
472                return this;
473        }
474
475        /**
476         * Populates given start, stop, step arrays from given slice array
477         * 
478         * @param slice
479         *            Input array of Slices wanted to convert
480         * @param shape
481         *            Input array of Slices shapes
482         * @param start
483         *            Output array of Slices starts
484         * @param stop
485         *            Output array of Slices stops
486         * @param step
487         *            Output array of Slices steps
488         */
489        public static void convertFromSlice(final Slice[] slice, final int[] shape, final int[] start, final int[] stop,
490                        final int[] step) {
491                final int rank = shape.length;
492                final int length = slice == null ? 0 : slice.length;
493
494                int i = 0;
495                for (; i < length; i++) {
496                        if (length > rank)
497                                break;
498
499                        Slice s = slice[i];
500                        if (s == null) {
501                                start[i] = 0;
502                                stop[i] = shape[i];
503                                step[i] = 1;
504                                continue;
505                        }
506                        int n;
507                        if (s.start == null) {
508                                start[i] = s.step > 0 ? 0 : shape[i] - 1;
509                        } else {
510                                n = s.start;
511                                if (n < 0)
512                                        n += shape[i];
513                                if (n < 0 || n >= shape[i]) {
514                                        throw new IllegalArgumentException(
515                                                        String.format("Start is out of bounds: %d is not in [%d,%d)", n, s.start, shape[i]));
516                                }
517                                start[i] = n;
518                        }
519
520                        if (s.stop == null) {
521                                stop[i] = s.step > 0 ? shape[i] : -1;
522                        } else {
523                                n = s.stop;
524                                if (n < 0)
525                                        n += shape[i];
526                                if (n < 0 || n > shape[i]) {
527                                        throw new IllegalArgumentException(
528                                                        String.format("Stop is out of bounds: %d is not in [%d,%d)", n, s.stop, shape[i]));
529                                }
530                                stop[i] = n;
531                        }
532
533                        n = s.step;
534                        if (n == 0) {
535                                throw new IllegalArgumentException("Step cannot be zero");
536                        }
537                        if (n > 0) {
538                                if (start[i] > stop[i])
539                                        throw new IllegalArgumentException("Start must be less than stop for positive steps");
540                        } else {
541                                if (start[i] < stop[i])
542                                        throw new IllegalArgumentException("Start must be greater than stop for negative steps");
543                        }
544                        step[i] = n;
545                }
546                for (; i < rank; i++) {
547                        start[i] = 0;
548                        stop[i] = shape[i];
549                        step[i] = 1;
550                }
551        }
552
553        /**
554         * Converts a set of integer arrays in a slice array
555         * 
556         * @param start
557         *            Array of Slices starts
558         * @param stop
559         *            Array of Slices stops
560         * @param step
561         *            Array of Slices steps
562         * @return Slice array corresponding to the starts, stops and steps arrays
563         *         entered.
564         */
565        public static Slice[] convertToSlice(final int[] start, final int[] stop, final int[] step) {
566                int orank = start.length;
567
568                if (stop.length != orank || step.length != orank) {
569                        throw new IllegalArgumentException("All arrays must be same length");
570                }
571
572                Slice[] slice = new Slice[orank];
573
574                for (int j = 0; j < orank; j++) {
575                        slice[j] = new Slice(start[j], stop[j], step[j]);
576                }
577
578                return slice;
579        }
580
581        /**
582         * Converts string in a Slice array
583         * 
584         * @param sliceString
585         *            String of the Slice array
586         * @return Slice array created from the given string
587         */
588        public static Slice[] convertFromString(String sliceString) {
589
590                String clean = sliceString.replace("[", "").replace("]", "");
591
592                String[] sub = clean.split(",");
593
594                Slice[] slices = new Slice[sub.length];
595
596                for (int i = 0; i < sub.length; i++) {
597                        String s = sub[i];
598
599                        Slice slice = new Slice();
600                        slices[i] = slice;
601
602                        int idx0 = s.indexOf(":");
603
604                        int n = 0;
605                        if (idx0 == -1) {
606                                n = Integer.parseInt(s);
607                                slice.setStart(n);
608                                slice.setStop(n + 1);
609                                continue;
610                        } else if (idx0 != 0) {
611                                n = Integer.parseInt(s.substring(0, idx0));
612                        }
613                        slice.setStart(n);
614
615                        idx0++;
616                        int idx1 = s.indexOf(":", idx0);
617                        if (idx1 == -1) {
618                                String t = s.substring(idx0).trim();
619                                if (t.length() == 0)
620                                        continue;
621
622                                slice.setStop(Integer.parseInt(t));
623                                continue;
624                        } else if (idx1 != idx0) {
625                                slice.setStop(Integer.parseInt(s.substring(idx0, idx1)));
626                        }
627
628                        String t = s.substring(idx1 + 1).trim();
629                        if (t.length() > 0)
630                                slice.setStep(Integer.parseInt(t));
631                }
632
633                return slices;
634        }
635
636        /**
637         * Creates a string representing the slice taken from given shape
638         * 
639         * @param shape
640         *            Array of Slices shapes
641         * @param start
642         *            Array of Slices starts
643         * @param stop
644         *            Array of Slices stops
645         * @param step
646         *            Array of Slices steps
647         * @return String representation of the Slice
648         */
649        public static String createString(final int[] shape, final int[] start, final int[] stop, final int[] step) {
650                final int rank = shape.length;
651                if (rank == 0) {
652                        return "";
653                }
654                StringBuilder s = new StringBuilder();
655                for (int i = 0; i < rank; i++) {
656                        int l = shape[i];
657                        int d = step == null ? 1 : step[i];
658                        int b = start == null ? (d > 0 ? 0 : l - 1) : start[i];
659                        int e = stop == null ? (d > 0 ? l : -1) : stop[i];
660
661                        appendSliceToString(s, l, b, e, d);
662                        s.append(',');
663                }
664
665                return s.substring(0, s.length() - 1);
666        }
667
668        /**
669         * Creates a string representation of slices.
670         * 
671         * @param slices
672         *            Wanted Slices to put inside of the string representation
673         * @return Return the string representation of the Slices entered.
674         */
675        public static String createString(Slice... slices) {
676                if (slices == null || slices.length == 0) {
677                        return "";
678                }
679
680                StringBuilder t = new StringBuilder();
681                for (Slice s : slices) {
682                        t.append(s != null ? s.toString() : ':');
683                        t.append(',');
684                }
685
686                return t.substring(0, t.length() - 1);
687        }
688}