    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         * Append 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        private static final int COLON = ':';
    582
    583        /**
    584         * Converts string in a Slice array
    585         *
    586         * @param sliceString
    587         *            String of the Slice array
    588         * @return Slice array created from the given string
    589         */
    590        public static Slice[] convertFromString(String sliceString) {
    591
    592                String clean = sliceString.replace("[", "").replace("]", "");
    593
    594                String[] sub = clean.split(",");
    595
    596                Slice[] slices = new Slice[sub.length];
    597
    598                for (int i = 0; i < sub.length; i++) {
    599                        String s = sub[i];
    600
    601                        Slice slice = new Slice();
    602                        slices[i] = slice;
    603
    604                        int idx0 = s.indexOf(COLON);
    605
    606                        int n = 0;
    607                        if (idx0 == -1) {
    608                                n = Integer.parseInt(s);
    609                                slice.setStart(n);
    610                                slice.setStop(n + 1);
    611                                continue;
    612                        } else if (idx0 != 0) {
    613                                n = Integer.parseInt(s.substring(0, idx0));
    614                        }
    615                        slice.setStart(n);
    616
    617                        idx0++;
    618                        int idx1 = s.indexOf(COLON, idx0);
    619                        if (idx1 == -1) {
    620                                String t = s.substring(idx0).trim();
    621                                if (t.length() == 0) {
    622                                        continue;
    623                                }
    624
    625                                slice.setStop(Integer.parseInt(t));
    626                                continue;
    627                        } else if (idx1 != idx0) {
    628                                slice.setStop(Integer.parseInt(s.substring(idx0, idx1)));
    629                        }
    630
    631                        String t = s.substring(idx1 + 1).trim();
    632                        if (t.length() > 0) {
    633                                slice.setStep(Integer.parseInt(t));
    634                        }
    635                }
    636
    637                return slices;
    638        }
    639
    640        /**
    641         * Creates a string representing the slice taken from given shape
    642         *
    643         * @param shape
    644         *            Array of Slices shapes
    645         * @param start
    646         *            Array of Slices starts
    647         * @param stop
    648         *            Array of Slices stops
    649         * @param step
    650         *            Array of Slices steps
    651         * @return String representation of the Slice
    652         */
    653        public static String createString(final int[] shape, final int[] start, final int[] stop, final int[] step) {
    654                final int rank = shape.length;
    655                if (rank == 0) {
    656                        return "";
    657                }
    658                StringBuilder s = new StringBuilder();
    659                for (int i = 0; i < rank; i++) {
    660                        int l = shape[i];
    661                        int d = step == null ? 1 : step[i];
    662                        int b = start == null ? (d > 0 ? 0 : l - 1) : start[i];
    663                        int e = stop == null ? (d > 0 ? l : -1) : stop[i];
    664
    665                        appendSliceToString(s, l, b, e, d);
    666                        s.append(',');
    667                }
    668
    669                return s.substring(0, s.length() - 1);
    670        }
    671
    672        /**
    673         * Creates a string representation of slices.
    674         *
    675         * @param slices
    676         *            Wanted Slices to put inside of the string representation
    677         * @return Return the string representation of the Slices entered.
    678         */
    679        public static String createString(Slice... slices) {
    680                if (slices == null || slices.length == 0) {
    681                        return "";
    682                }
    683
    684                StringBuilder t = new StringBuilder();
    685                for (Slice s : slices) {
    686                        t.append(s != null ? s.toString() : ':');
    687                        t.append(',');
    688                }
    689
    690                return t.substring(0, t.length() - 1);
    691        }
    692}