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 && step > 0 && length < stop) {
146                        throw new IllegalArgumentException("Length must be greater than or equal to stop");
147                }
148                if (start != null && step < 0 && length < start) {
149                        throw new IllegalArgumentException("Length must be greater than or equal to start");
150                }
151                this.length = length;
152                return this;
153        }
154
155        /**
156         * Returns {@code true} if the slice has a maximum size equal to the current
157         * size, else {@code false}.
158         * 
159         * @return {@code true} if slice represents complete dimension,
160         *         {@code false} in the other case.
161         */
162        public boolean isSliceComplete() {
163                if (start == null && stop == null && (step == 1 || step == -1))
164                        return true;
165                if (length > 0) {
166                        return getNumSteps() == length;
167                }
168
169                return true;
170        }
171
172        /**
173         * Returns the maximum value of the slice.
174         * 
175         * @return Maximum value of the slice
176         */
177        public int getLength() {
178                return length;
179        }
180
181        /**
182         * Returns the starting index of the slice.
183         * 
184         * @return Start point of the slice
185         */
186        public Integer getStart() {
187                return start;
188        }
189
190        /**
191         * Returns the stopping index of the slice.
192         * 
193         * @return Stop point of the slice
194         */
195        public Integer getStop() {
196                return stop;
197        }
198
199        /**
200         * Returns the step of the slice.
201         * 
202         * @return Step of the slice
203         */
204        public int getStep() {
205                return step;
206        }
207
208        /**
209         * Set the starting index of the slice. If the start point of the Slice is
210         * {@code null}, it will be set automatically to 0.
211         * 
212         * @param start
213         *            Starting index of the Slice, may be {@code null}
214         */
215        public void setStart(Integer start) {
216                if (start != null && length > 0) {
217                        if (step > 0) {
218                                int end = stop == null ? length : stop;
219                                if (start >= end) {
220                                        throw new IllegalArgumentException("Non-null start must be less than end");
221                                }
222                        } else {
223                                int end = stop == null ? -1 : stop;
224                                if (start < end) {
225                                        throw new IllegalArgumentException("Non-null start must be greater than end for negative step");
226                                }
227                        }
228                }
229                this.start = start;
230        }
231
232        /**
233         * Set the stopping index of the slice. If the stop point of the Slice is
234         * {@code null}, it will be set to the stop argument default to the end of
235         * the dimension that the slice is applied to.
236         * 
237         * @param stop
238         *            Stopping index of the Slice, may be {@code null}
239         */
240        public void setStop(Integer stop) {
241                if (stop != null && length > 0) {
242                        if (step > 0) {
243                                int beg = start == null ? 0 : start;
244                                if (stop < beg) {
245                                        throw new IllegalArgumentException("Non-null stop must be greater than or equal to beginning");
246                                }
247                        } else {
248                                int beg = start == null ? length - 1 : start;
249                                if (stop >= beg) {
250                                        throw new IllegalArgumentException("Non-null stop must be less than beginning for negative step");
251                                }
252                        }
253                        if (stop > length)
254                                stop = length;
255                }
256                this.stop = stop;
257        }
258
259        /**
260         * Move the start and end to an other index keeping the same step and the
261         * same gap between the two values
262         * 
263         * @param beg
264         *            New starting point
265         * @return Return {@code true} if the end was reached, {@code false} in the
266         *         other case.
267         */
268        public boolean setPosition(int beg) {
269                boolean end = false;
270                int len = getNumSteps();
271                int max = getNumSteps(beg, length, step);
272                if (len > max) {
273                        len = max;
274                        end = true;
275                }
276                start = beg;
277                stop = start + (len - 1) * step + 1;
278                return end;
279        }
280
281        /**
282         * Returns the index of the n-th step inside of the slice
283         * 
284         * @param n
285         *            Wanted step index in the slice
286         * @return Return the index of the step inside of the Slice
287         */
288        public int getPosition(int n) {
289                if (n < 0)
290                        throw new IllegalArgumentException("Given n-th step should be non-negative");
291                if (n >= getNumSteps())
292                        throw new IllegalArgumentException("N-th step exceeds extent of slice");
293                int beg;
294                if (start == null) {
295                        if (step < 0) {
296                                if (length < 0) {
297                                        if (stop == null) {
298                                                throw new IllegalStateException("Length or stop should be set");
299                                        }
300                                        beg = stop - 1;
301                                } else {
302                                        beg = length - 1;
303                                }
304                        } else {
305                                beg = 0;
306                        }
307                } else {
308                        beg = start;
309                }
310                return beg + step * n;
311        }
312
313        /**
314         * Set the step size inside of the Slice. If the the wanted step is set to
315         * {@code null}, it will be set by default to 1.
316         * 
317         * @param step
318         *            New wanted step, may be {@code null}
319         */
320        public void setStep(int step) {
321                if (step == 0) {
322                        throw new IllegalArgumentException("Step must not be zero");
323                }
324                this.step = step;
325        }
326
327        /**
328         * Returns a String representation of the Slice comparable to the python
329         * representation.
330         * 
331         * @param s
332         *            String builder
333         * @param len
334         *            Maximal length of the Slice, or -1 if not set
335         * @param beg
336         *            Start index of the Slice
337         * @param end
338         *            Stop index of the Slice
339         * @param del
340         *            Step of the Slice
341         */
342        public static void appendSliceToString(final StringBuilder s, final int len, final int beg, final int end,
343                        final int del) {
344                int o = s.length();
345                if (del > 0) {
346                        if (beg != 0)
347                                s.append(beg);
348                } else {
349                        if (beg != len - 1)
350                                s.append(beg);
351                }
352
353                int n = getNumSteps(beg, end, del);
354                if (n == 1) {
355                        if (s.length() == o) {
356                                s.append(beg);
357                        }
358                        return;
359                }
360
361                s.append(':');
362
363                if (del > 0) {
364                        if (end != len)
365                                s.append(end);
366                } else {
367                        if (end != -1)
368                                s.append(end);
369                }
370
371                if (del != 1) {
372                        s.append(':');
373                        s.append(del);
374                }
375        }
376
377        /**
378         * Returns a string construction of the slice with the python form.
379         * 
380         * @return Constructed String.
381         */
382        @Override
383        public String toString() {
384                StringBuilder s = new StringBuilder();
385                appendSliceToString(s, length, start != null ? start : (step > 0 ? 0 : length - 1),
386                                stop != null ? stop : (step > 0 ? length : -1), step);
387                return s.toString();
388        }
389
390        /**
391         * Returns the number of steps inside of the Slice
392         * 
393         * @return Number of steps inside of the Slice
394         */
395        public int getNumSteps() {
396                if (length < 0) {
397                        if (stop == null)
398                                throw new IllegalStateException("Slice is underspecified - stop is null and length is negative");
399                        int beg = start == null ? (step > 0 ? 0 : stop - 1) : start;
400                        if (step > 0 && stop <= beg)
401                                return 0;
402                        if (step < 0 && stop > beg)
403                                return 0;
404                        return getNumSteps(beg, stop, step);
405                }
406                int beg = start == null ? (step > 0 ? 0 : length - 1) : start;
407                int end = stop == null ? (step > 0 ? length : -1) : stop;
408                return getNumSteps(beg, end, step);
409        }
410
411        /**
412         * Returns the number of steps inside of the Slice from a point to an other
413         * point minus 1 because this is an exclusive index
414         * 
415         * @param beg
416         *            Starting point
417         * @param end
418         *            (exclusive) Stopping point
419         * @return Numbers of steps between the 2 limits.
420         */
421        public int getNumSteps(int beg, int end) {
422                return getNumSteps(beg, end, step);
423        }
424
425        private static int getNumSteps(int beg, int end, int step) {
426                int del = step > 0 ? 1 : -1;
427                return Math.max(0, (end - beg - del) / step + 1);
428        }
429
430        /**
431         * Returns the last value inside of Slice.
432         * 
433         * @return Last value in the slice, it can be a lower value than the start
434         *         if the step is going backward
435         */
436        public int getEnd() {
437                int n = getNumSteps() - 1;
438                if (n < 0)
439                        throw new IllegalStateException("End is not defined");
440
441                return getPosition(n);
442        }
443
444        /**
445         * Flips the Slice direction, after this operation, the slice begins at
446         * previous end point, steps in the opposite direction, and finishes at the
447         * previous start point.
448         * <p>
449         * Note : the stop value may not be preserved across two flips
450         * </p>
451         * 
452         * @return Flipped Slice.
453         */
454        public Slice flip() {
455                if (length < 0) {
456                        Integer tmp = start == null ? null : start - step;
457                        start = stop == null ? null : getEnd();
458                        stop = tmp;
459                } else {
460                        Integer tstart = start;
461                        start = stop == null ? null : getEnd();
462                        stop = tstart == null ? null : tstart - step;
463                }
464                step = -step;
465
466                return this;
467        }
468
469        /**
470         * Populates given start, stop, step arrays from given slice array
471         * 
472         * @param slice
473         *            Input array of Slices wanted to convert
474         * @param shape
475         *            Input array of Slices shapes
476         * @param start
477         *            Output array of Slices starts
478         * @param stop
479         *            Output array of Slices stops
480         * @param step
481         *            Output array of Slices steps
482         */
483        public static void convertFromSlice(final Slice[] slice, final int[] shape, final int[] start, final int[] stop,
484                        final int[] step) {
485                final int rank = shape.length;
486                final int length = slice == null ? 0 : slice.length;
487
488                int i = 0;
489                for (; i < length; i++) {
490                        if (length > rank)
491                                break;
492
493                        Slice s = slice[i];
494                        if (s == null) {
495                                start[i] = 0;
496                                stop[i] = shape[i];
497                                step[i] = 1;
498                                continue;
499                        }
500                        int n;
501                        if (s.start == null) {
502                                start[i] = s.step > 0 ? 0 : shape[i] - 1;
503                        } else {
504                                n = s.start;
505                                if (n < 0)
506                                        n += shape[i];
507                                if (n < 0 || n >= shape[i]) {
508                                        throw new IllegalArgumentException(
509                                                        String.format("Start is out of bounds: %d is not in [%d,%d)", n, s.start, shape[i]));
510                                }
511                                start[i] = n;
512                        }
513
514                        if (s.stop == null) {
515                                stop[i] = s.step > 0 ? shape[i] : -1;
516                        } else {
517                                n = s.stop;
518                                if (n < 0)
519                                        n += shape[i];
520                                if (n < 0 || n > shape[i]) {
521                                        throw new IllegalArgumentException(
522                                                        String.format("Stop is out of bounds: %d is not in [%d,%d)", n, s.stop, shape[i]));
523                                }
524                                stop[i] = n;
525                        }
526
527                        n = s.step;
528                        if (n == 0) {
529                                throw new IllegalArgumentException("Step cannot be zero");
530                        }
531                        if (n > 0) {
532                                if (start[i] > stop[i])
533                                        throw new IllegalArgumentException("Start must be less than stop for positive steps");
534                        } else {
535                                if (start[i] < stop[i])
536                                        throw new IllegalArgumentException("Start must be greater than stop for negative steps");
537                        }
538                        step[i] = n;
539                }
540                for (; i < rank; i++) {
541                        start[i] = 0;
542                        stop[i] = shape[i];
543                        step[i] = 1;
544                }
545        }
546
547        /**
548         * Converts a set of integer arrays in a slice array
549         * 
550         * @param start
551         *            Array of Slices starts
552         * @param stop
553         *            Array of Slices stops
554         * @param step
555         *            Array of Slices steps
556         * @return Slice array corresponding to the starts, stops and steps arrays
557         *         entered.
558         */
559        public static Slice[] convertToSlice(final int[] start, final int[] stop, final int[] step) {
560                int orank = start.length;
561
562                if (stop.length != orank || step.length != orank) {
563                        throw new IllegalArgumentException("All arrays must be same length");
564                }
565
566                Slice[] slice = new Slice[orank];
567
568                for (int j = 0; j < orank; j++) {
569                        slice[j] = new Slice(start[j], stop[j], step[j]);
570                }
571
572                return slice;
573        }
574
575        /**
576         * Converts string in a Slice array
577         * 
578         * @param sliceString
579         *            String of the Slice array
580         * @return Slice array created from the given string
581         */
582        public static Slice[] convertFromString(String sliceString) {
583
584                String clean = sliceString.replace("[", "");
585                clean = clean.replace("]", "");
586
587                String[] sub = clean.split(",");
588
589                Slice[] slices = new Slice[sub.length];
590
591                for (int i = 0; i < sub.length; i++) {
592                        String s = sub[i];
593
594                        Slice slice = new Slice();
595                        slices[i] = slice;
596
597                        int idx0 = s.indexOf(":");
598
599                        int n = 0;
600                        if (idx0 == -1) {
601                                n = Integer.parseInt(s);
602                                slice.setStart(n);
603                                slice.setStop(n + 1);
604                                continue;
605                        } else if (idx0 != 0) {
606                                n = Integer.parseInt(s.substring(0, idx0));
607                        }
608                        slice.setStart(n);
609
610                        idx0++;
611                        int idx1 = s.indexOf(":", idx0);
612                        if (idx1 == -1) {
613                                String t = s.substring(idx0).trim();
614                                if (t.length() == 0)
615                                        continue;
616
617                                slice.setStop(Integer.parseInt(t));
618                                continue;
619                        } else if (idx1 != idx0) {
620                                slice.setStop(Integer.parseInt(s.substring(idx0, idx1)));
621                        }
622
623                        String t = s.substring(idx1 + 1).trim();
624                        if (t.length() > 0)
625                                slice.setStep(Integer.parseInt(t));
626                }
627
628                return slices;
629        }
630
631        /**
632         * Creates a string representing the slice taken from given shape
633         * 
634         * @param shape
635         *            Array of Slices shapes
636         * @param start
637         *            Array of Slices starts
638         * @param stop
639         *            Array of Slices stops
640         * @param step
641         *            Array of Slices steps
642         * @return String representation of the Slice
643         */
644        public static String createString(final int[] shape, final int[] start, final int[] stop, final int[] step) {
645                final int rank = shape.length;
646                if (rank == 0) {
647                        return "";
648                }
649                StringBuilder s = new StringBuilder();
650                for (int i = 0; i < rank; i++) {
651                        int l = shape[i];
652                        int d = step == null ? 1 : step[i];
653                        int b = start == null ? (d > 0 ? 0 : l - 1) : start[i];
654                        int e = stop == null ? (d > 0 ? l : -1) : stop[i];
655
656                        appendSliceToString(s, l, b, e, d);
657                        s.append(',');
658                }
659
660                return s.substring(0, s.length() - 1);
661        }
662
663        /**
664         * Creates a string representation of slices.
665         * 
666         * @param slices
667         *            Wanted Slices to put inside of the string representation
668         * @return Return the string representation of the Slices entered.
669         */
670        public static String createString(Slice... slices) {
671                if (slices == null || slices.length == 0) {
672                        return "";
673                }
674
675                StringBuilder t = new StringBuilder();
676                for (Slice s : slices) {
677                        t.append(s != null ? s.toString() : ':');
678                        t.append(',');
679                }
680
681                return t.substring(0, t.length() - 1);
682        }
683}