Сдвиг выбранных элементов на один и возврат их обновленных индексов - PullRequest
0 голосов
/ 09 июня 2018

Справочная информация:

Недавно я декомпилировал мой старый проект, исходный код которого я потерял, и я реорганизую весь этот ужасный код, который я написал несколько лет назад.

У меня есть список с элементами, отображаемыми пользовательским интерфейсом, элементы можно перемещать вверх и вниз, и разрешен множественный выбор.У меня есть метод int[] moveUp(int[] selectedIndices), который обновляет модель и возвращает новые индексы смещенных элементов в списке (чтобы я мог обновить пользовательский интерфейс после изменения модели).

selectedIndices происходит от JList#getSelectedIndices, которыйгарантирует отсортированный заказ без повторений.

Старое решение:

public int[] moveUp(int[] selectedIndices) {
    Action[] array = concatenate(new Action[]{null}, actions.toArray(new Action[0]));
    for (int i = 0; i < selectedIndices.length; i++) {
        swap(array, selectedIndices[i] + 1, selectedIndices[i]);
        selectedIndices[i] -= 1;
    }
    actions = new ArrayList<>(this.actions.size());
    for (Action action : array) {
        if (action != null) {
            actions.add(action);
        }
    }
    return selectedIndices;
}

Проблема:

Если у меня есть действия[a, b, c, d] и selectedIndices равны [0, 1, 3], хотя результат будет правильным ([a, b, d, c]), возвращенные новые индексы будут [-1, 0, 2], тогда как они должны быть [0, 1, 2].

Новое решение:

public int[] moveUp(int[] selectedIndices) {
    List<Action> selectedActions = stream(selectedIndices).mapToObj(actions::get).collect(toList());

    actions.add(0, null);
    for (int selectedIndex : selectedIndices) {
        swap(actions, selectedIndex + 1, selectedIndex);
    }
    actions.remove(null);

    return selectedActions.stream().mapToInt(actions::indexOf).toArray();
}

Проблема:

Это не чисто и не эффективно.

Вопрос:

Как реализовать это чистым и эффективным способом?

MCVE и тесты:

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.shazam</groupId>
        <artifactId>shazamcrest</artifactId>
        <version>0.11</version>
        <scope>test</scope>
    </dependency>
</dependencies>

/////////////////////////////////////////////////////////////////////////

public interface Action {

}

/////////////////////////////////////////////////////////////////////////

import java.util.ArrayList;
import java.util.List;

import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;

public class Actions {

    private List<Action> actions = new ArrayList<>();

    public void add(Action action) {
        this.actions.add(action);
    }

    public int[] moveUp(int[] selectedIndices) {
        List<Action> selectedActions = stream(selectedIndices).mapToObj(actions::get).collect(toList());

        actions.add(0, null);
        for (int selectedIndex : selectedIndices) {
            swap(actions, selectedIndex + 1, selectedIndex);
        }
        actions.remove(null);

        return selectedActions.stream().mapToInt(actions::indexOf).toArray();
    }

    private static <T> void swap(List<T> list, int index, int index2) {
        T t = list.get(index);
        list.set(index, list.get(index2));
        list.set(index2, t);
    }

}

/////////////////////////////////////////////////////////////////////////

public class FakeAction implements Action {

    @SuppressWarnings("FieldCanBeLocal") // used by shazamcrest
    private final String name;

    private FakeAction(String name) {
        this.name = name;
    }

    static Actions actionsWithElements(int... elementsNames) {
        Actions actions = new Actions();
        for (int elementName : elementsNames) {
            actions.add(new FakeAction(String.valueOf(elementName)));
        }
        return actions;
    }

    static int[] indices(int... indices) {
        return indices;
    }

}

/////////////////////////////////////////////////////////////////////////

import org.junit.Test;

import static com.shazam.shazamcrest.MatcherAssert.assertThat;
import static com.shazam.shazamcrest.matcher.Matchers.sameBeanAs;
import static FakeAction.actionsWithElements;
import static FakeAction.indices;

public class ActionsMoveUpTest {

    @Test
    public void movesUpSingleAction() {
        Actions actions = actionsWithElements(0, 1, 2, 3);

        actions.moveUp(indices(2));

        assertThat(actions, sameBeanAs(actionsWithElements(0, 2, 1, 3)));
    }

    @Test
    public void movesUpMultipleActions() {
        Actions actions = actionsWithElements(0, 1, 2, 3);

        actions.moveUp(indices(1, 3));

        assertThat(actions, sameBeanAs(actionsWithElements(1, 0, 3, 2)));
    }

    @Test
    public void doesNothingWhenArrayOfIndicesToMoveUpIsEmpty() {
        Actions actions = actionsWithElements(0, 1, 2, 3);

        actions.moveUp(indices());

        assertThat(actions, sameBeanAs(actionsWithElements(0, 1, 2, 3)));
    }

    @Test
    public void doesNotLoseSelectionWhenMovingUpTheTopAction() {
        Actions actions = actionsWithElements(0, 1, 2, 3);

        int[] newIndices = actions.moveUp(indices(0));

        assertThat(newIndices, sameBeanAs(indices(0)));
    }

    @Test
    public void movesUpOnlyThoseActionsWhichAreNotOnTheTopAlready() {
        Actions actions = actionsWithElements(0, 1, 2, 3, 4, 5, 6);

        actions.moveUp(indices(0, 1, 4, 6));

        assertThat(actions, sameBeanAs(actionsWithElements(0, 1, 2, 4, 3, 6, 5)));
    }

    @Test
    public void doesNotLoseSelectionOfTheTopActionsWhenMovingMultipleActions() {
        Actions actions = actionsWithElements(0, 1, 2, 3, 4, 5, 6);

        int[] newIndices = actions.moveUp(indices(0, 1, 4, 6));

        assertThat(newIndices, sameBeanAs(indices(0, 1, 3, 5)));
    }

}

Ответы [ 2 ]

0 голосов
/ 12 июня 2018

Благодаря наблюдению Миши я пришел с чем-то еще более простым.

Ключ в том, что если selectedIndices[i] == i, элемент не двигается:

actions:               [a, b, c, d, e, f, g, h, i]
index:                  0  1  2  3  4  5  6  7  8
selectedIndices:       [0, 1, 2,       5, 6,    8]
elements to move left:                 f  g     i
output:                [a, b, c, d, f, g, e, i, h]

Отсюда, этоможет быть легко реализовано с O(m) сложностью времени (где m = selectedIndices.length) как:

public int[] moveUp(int[] selectedIndices) {
    int[] newIndices = Arrays.copyOf(selectedIndices, selectedIndices.length);

    for (int i = 0; i < selectedIndices.length; i++) {
        if (selectedIndices[i] != i) {
            swap(actions, selectedIndices[i], selectedIndices[i] - 1);
            newIndices[i]--;
        }
    }

    return newIndices;
}

Или с некоторыми потоками Java:

public int[] moveUp(int[] selectedIndices) {
    int[] newIndices = Arrays.copyOf(selectedIndices, selectedIndices.length);

    IntStream.range(0, selectedIndices.length)
            .filter(i -> selectedIndices[i] != i)
            .forEach(i -> {
                swap(actions, selectedIndices[i], selectedIndices[i] - 1);
                newIndices[i]--;
            });

    return newIndices;
}
0 голосов
/ 09 июня 2018

Поскольку вы предполагаете, что selectedIndices отсортировано, вы можете легко определить, какие элементы следует переместить вверх, сравнив индекс с его положением в selectedIndices.Если индекс выше, чем его позиция, связанный элемент должен быть перемещен.

public int[] moveUp(int[] selecteIndices) {
    // this assumes that selectedIndices is sorted
    int[] newSelection = IntStream.range(0, selectedIndices.length)
        .map(x -> selectedIndices[x] > x ? selectedIndices[x] - 1 : selectedIndices[x])
        .toArray();

    IntStream.range(0, selectedIndices.length)
        .filter(x -> selectedIndices[x] > x)
        .map(x -> selectedIndices[x])
        .forEachOrdered(i -> swap(actions, i - 1, i));

    return newSelection;
}

Чтобы свести к минимуму количество психической энергии, затрачиваемой на размышления об «индексе индекса», вы можете отдельно рассчитатьпервый элемент, который будет перемещен.Для меня это понятнее, но это субъективно:

public int[] moveUp(int[] selecteIndices) {
    // this assumes that selecteIndices are sorted

    // index of the first element to move up or actions.size() if nothing needs to move
    int firstToMove = IntStream.range(0, selectedIndices.length)
        .filter(x -> selectedIndices[x] > x)
        .map(x -> selectedIndices[x])
        .findFirst()
        .orElse(actions.size());  

    int[] newSelection = Arrays.stream(selectedIndices)
        .map(i -> i >= firstToMove ? i - 1 : i)
        .toArray();

    Arrays.stream(selectedIndices)
        .filter(i -> i >= firstToMove)
        .forEachOrdered(i -> swap(actions, i - 1, i));

    return newSelection;
}
...