Java 8에서 인덱스를 사용하여 스트림을 반복하는 간결한 방법이 있습니까?


382

스트림의 인덱스에 액세스하면서 스트림을 반복하는 간결한 방법이 있습니까?

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = intRange(1, names.length).boxed();
nameList = zip(indices, stream(names), SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey())
        .map(Entry::getValue)
        .collect(toList());

LINQ 예제에 비해 다소 실망스러워 보입니다.

string[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
var nameList = names.Where((c, index) => c.Length <= index + 1).ToList();

더 간결한 방법이 있습니까?

또한 지퍼가 움직이거나 제거 된 것 같습니다 ...


2
무엇입니까 intRange()? 지금까지 Java 8 에서이 방법을 사용하지 않았습니다.
Rohit Jain

@RohitJain 아마 an IntStream.rangeClosed(x, y).
assylias

2
부수적으로, 도전 4는 다음과 같이 더 좋아 보인다 (IMO)List<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
assylias

3
예, zip다양한 값을 갖는 실험적인 값이있는 스트림과 함께 BiStream또는 이라고 제거되었습니다 MapStream. 주된 문제는이 작업을 효과적으로 수행하기 위해서는 실제로 구조적으로 형식화 된 쌍 (또는 튜플) 형식이 필요하다는 것입니다. 하나가 없으면 일반 페어 또는 튜플 클래스를 쉽게 만들 수 있습니다. 여러 번 수행되었지만 모두 같은 유형으로 지워집니다.
Stuart Marks

3
아, 일반적인 Pair 또는 Tuple 클래스의 또 다른 문제는 모든 프리미티브를 박스로 만들어야한다는 것입니다.
Stuart Marks

답변:


435

가장 깨끗한 방법은 일련의 인덱스에서 시작하는 것입니다.

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
IntStream.range(0, names.length)
         .filter(i -> names[i].length() <= i)
         .mapToObj(i -> names[i])
         .collect(Collectors.toList());

결과 목록에는 "Erik"만 포함됩니다.


루프에 익숙 할 때 더 친숙해 보이는 한 가지 대안은 다음과 같이 변경 가능한 객체를 사용하여 임시 카운터를 유지하는 것입니다 AtomicInteger.

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
AtomicInteger index = new AtomicInteger();
List<String> list = Arrays.stream(names)
                          .filter(n -> n.length() <= index.incrementAndGet())
                          .collect(Collectors.toList());

병렬 스트림에서 후자의 방법사용하면 항목이 "순서대로"처리되지 않기 때문에 중단 될 수 있습니다 .


28
이런 방식으로 원자를 사용하는 것은 병렬 스트림에서 문제가됩니다. 첫째, 요소 처리 순서가 요소가 초기 배열에서 발생하는 순서와 반드시 같을 필요는 없습니다. 따라서 원자를 사용하여 할당 된 "인덱스"는 실제 배열 인덱스와 일치하지 않을 수 있습니다. 둘째, 원자는 스레드로부터 안전하지만 여러 스레드에서 원자를 업데이트하는 경합이 발생하여 병렬 처리 수준이 저하 될 수 있습니다.
Stuart Marks

1
@assylias의 솔루션과 비슷한 솔루션을 개발했습니다. 언급 된 병렬 스트림 @StuartMarks의 문제를 피하기 위해 먼저 주어진 병렬 스트림을 순차적으로 만들고 매핑을 수행하고 병렬 상태를 복원합니다. public static <T> Stream<Tuple2<Integer, T>> zipWithIndex(Stream<T> stream) { final AtomicInteger index = new AtomicInteger(); final Function<T, Tuple2<Integer, T>> zipper = e -> Tuples.of(index.getAndIncrement(), e); if (stream.isParallel()) { return stream.sequential().map(zipper).parallel(); } else { return stream.map(zipper); } }
Daniel Dietrich

4
@DanielDietrich 문제가 해결되었다고 생각되면 의견이 아니라 답변으로 게시해야합니다 (코드도 더 읽기 쉽습니다).
assylias

3
@DanielDietrich 죄송합니다. 해당 코드를 올바르게 읽으면 작동하지 않습니다. 파이프 라인의 다른 세그먼트를 병렬 및 순차적으로 실행할 수 없습니다. 터미널 작업이 시작될 때 마지막 parallel또는 마지막이 적용 sequential됩니다.
스튜어트 마크

4
정의를 위해 "가장 깨끗한 방법"은 @Stuart의 답변에서 도난당했습니다.
Vadzim

70

Java 8 스트림 API에는 스트림 요소의 색인을 얻는 기능과 스트림을 함께 압축하는 기능이 없습니다. LINQ 과제와 같은 특정 응용 프로그램을 다른 방법보다 더 어렵게 만드는 것은 불행한 일입니다.

그러나 종종 해결 방법이 있습니다. 일반적으로 이것은 정수 범위의 스트림을 "구동"하고 원래 요소가 종종 배열 또는 색인으로 액세스 가능한 콜렉션에 있다는 사실을 이용하여 수행 할 수 있습니다. 예를 들어, Challenge 2 문제는 다음과 같이 해결할 수 있습니다.

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList =
    IntStream.range(0, names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(toList());

위에서 언급했듯이, 이것은 데이터 소스 (이름 배열)가 직접 인덱싱 가능하다는 사실을 이용합니다. 그렇지 않은 경우이 기술은 작동하지 않습니다.

이것이 도전 2의 의도를 만족시키지 못한다는 것을 인정할 것이다. 그럼에도 불구하고 그것이 문제를 합리적으로 효과적으로 해결한다.

편집하다

필자의 이전 코드 예제 flatMap는 필터 및 맵 작업을 통합하는 데 사용 되었지만 번거롭고 이점이 없었습니다. Holger의 의견에 따라 예제를 업데이트했습니다.


7
어때요 IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? 권투없이 작동합니다 ...
Holger

1
흠, 왜 내가 flatMap어쨌든 사용해야한다고 생각 했 습니까?
Stuart Marks

2
마지막으로 이것을 다시 방문 ... 아마도 flatMap필터링 및 매핑 작업을 단일 작업으로 융합하기 때문에 아마도 사용 되었지만 실제로는 이점이 없습니다. 예제를 편집하겠습니다.
Stuart Marks

Stream.of (Array)는 배열에 대한 스트림 인터페이스를 만듭니다. 효과적으로 Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );언 박싱 및 메모리 할당을 줄입니다. 더 이상 범위 스트림을 만들지 않기 때문입니다.
코드 아이즈

44

구아바 21부터 사용할 수 있습니다

Streams.mapWithIndex()

예 ( 공식 문서에서 ) :

Streams.mapWithIndex(
    Stream.of("a", "b", "c"),
    (str, index) -> str + ":" + index)
) // will return Stream.of("a:0", "b:1", "c:2")

3
또한 구아바 사람들은 아직 함수가 아닌 소비자를 위해 EachWithIndex를 구현하지 않았지만 할당 된 문제 인 github.com/google/guava/issues/2913 입니다.
John Glassmyer

25

내 프로젝트에서 다음 솔루션을 사용했습니다. 가변 객체 또는 정수 범위를 사용하는 것보다 낫다고 생각합니다.

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collector.Characteristics;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Objects.requireNonNull;


public class CollectionUtils {
    private CollectionUtils() { }

    /**
     * Converts an {@link java.util.Iterator} to {@link java.util.stream.Stream}.
     */
    public static <T> Stream<T> iterate(Iterator<? extends T> iterator) {
        int characteristics = Spliterator.ORDERED | Spliterator.IMMUTABLE;
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, characteristics), false);
    }

    /**
     * Zips the specified stream with its indices.
     */
    public static <T> Stream<Map.Entry<Integer, T>> zipWithIndex(Stream<? extends T> stream) {
        return iterate(new Iterator<Map.Entry<Integer, T>>() {
            private final Iterator<? extends T> streamIterator = stream.iterator();
            private int index = 0;

            @Override
            public boolean hasNext() {
                return streamIterator.hasNext();
            }

            @Override
            public Map.Entry<Integer, T> next() {
                return new AbstractMap.SimpleImmutableEntry<>(index++, streamIterator.next());
            }
        });
    }

    /**
     * Returns a stream consisting of the results of applying the given two-arguments function to the elements of this stream.
     * The first argument of the function is the element index and the second one - the element value. 
     */
    public static <T, R> Stream<R> mapWithIndex(Stream<? extends T> stream, BiFunction<Integer, ? super T, ? extends R> mapper) {
        return zipWithIndex(stream).map(entry -> mapper.apply(entry.getKey(), entry.getValue()));
    }

    public static void main(String[] args) {
        String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

        System.out.println("Test zipWithIndex");
        zipWithIndex(Arrays.stream(names)).forEach(entry -> System.out.println(entry));

        System.out.println();
        System.out.println("Test mapWithIndex");
        mapWithIndex(Arrays.stream(names), (Integer index, String name) -> index+"="+name).forEach((String s) -> System.out.println(s));
    }
}

+1-N 개의 인덱스를 사용하여 요소를 "삽입"하는 함수 StreamSupport.stream()와 사용자 정의 반복자 를 구현할 수있었습니다 .
ach

13

protonpack뿐만 아니라, jOOλ의 서열은 이 기능을 제공 (그리고 같은 그 위에 빌드 것을 확장 라이브러리가 외눈 박이가-반응 ,이 라이브러리의 저자).

Seq.seq(Stream.of(names)).zipWithIndex()
                         .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                         .toList();

Seq는 Seq.of (names) 만 지원하며 표지 아래에 JDK 스트림을 빌드합니다.

간단한 반응식과 비슷하게 보일 것입니다.

 LazyFutureStream.of(names)
                 .zipWithIndex()
                 .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                 .toList();

단순 반응 버전은 비동기 / 동시 처리에보다 적합합니다.


존 나는 오늘 당신의 서재를 보았습니다.
GOXR3PLUS

12

완성을 위해 StreamEx 라이브러리 와 관련된 솔루션이 있습니다.

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
EntryStream.of(names)
    .filterKeyValue((idx, str) -> str.length() <= idx+1)
    .values().toList();

여기서 우리 는 or 와 같은 특정 작업 EntryStream<Integer, String>을 확장 Stream<Entry<Integer, String>>하고 추가 하는를 만듭니다 . 또한 바로 가기가 사용됩니다.filterKeyValuevaluestoList()


훌륭한 일; 바로 가기가 .forEach(entry -> {}) 있습니까?
Steve Oh

2
@SteveOh 당신이 올바르게 질문을 이해하면, 예, 쓸 수 있습니다 .forKeyValue((key, value) -> {}).
Tagir Valeev 2016

8

스트림이 목록이나 배열로 만들어 졌을 때 해결책을 찾았습니다 (크기를 알고 있습니다). 그러나 스트림 크기를 알 수없는 경우 어떻게해야합니까? 이 경우 다음 변형을 시도하십시오.

public class WithIndex<T> {
    private int index;
    private T value;

    WithIndex(int index, T value) {
        this.index = index;
        this.value = value;
    }

    public int index() {
        return index;
    }

    public T value() {
        return value;
    }

    @Override
    public String toString() {
        return value + "(" + index + ")";
    }

    public static <T> Function<T, WithIndex<T>> indexed() {
        return new Function<T, WithIndex<T>>() {
            int index = 0;
            @Override
            public WithIndex<T> apply(T t) {
                return new WithIndex<>(index++, t);
            }
        };
    }
}

용법:

public static void main(String[] args) {
    Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
    stream.map(WithIndex.indexed()).forEachOrdered(e -> {
        System.out.println(e.index() + " -> " + e.value());
    });
}

6

당신이 시도 할 수있는 목록으로

List<String> strings = new ArrayList<>(Arrays.asList("First", "Second", "Third", "Fourth", "Fifth")); // An example list of Strings
strings.stream() // Turn the list into a Stream
    .collect(HashMap::new, (h, o) -> h.put(h.size(), o), (h, o) -> {}) // Create a map of the index to the object
        .forEach((i, o) -> { // Now we can use a BiConsumer forEach!
            System.out.println(String.format("%d => %s", i, o));
        });

산출:

0 => First
1 => Second
2 => Third
3 => Fourth
4 => Fifth

1
실제로 좋은 생각이지만 strings :: indexOf 는 약간 비쌀 수 있습니다. 대신 .collect (HashMap :: new, (h, s)-> h.put (h.size (), s), (h, s)-> {})을 사용하는 것이 좋습니다. size () 메서드를 사용하여 인덱스를 만들 수 있습니다.
gil.fernandes

@ gil.fernandes 제안 해 주셔서 감사합니다. 편집하겠습니다.
V0idst4r 2016 년

3

Streama Stream와는 다르기 때문에 인덱스에 액세스 하는 동안 반복하는 방법은 없습니다 Collection. A Stream문서에 명시된대로 한 장소에서 다른 장소로 데이터를 전달하기위한 파이프 라인 일뿐입니다 .

저장 공간이 없습니다. 스트림은 요소를 저장하는 데이터 구조가 아닙니다. 대신, 계산 연산의 파이프 라인을 통해 소스 (데이터 구조, 생성기, IO 채널 등일 수 있음)에서 값을 전달합니다.

물론, 같은 당신은 항상 당신을 변환 할 수 있습니다, 귀하의 질문에 암시 한 것으로 나타났습니다 Stream<V>A를 Collection<V>예로, List<V>있는 당신이 인덱스에 액세스 할 수 있습니다.


2
다른 언어 / 도구로도 제공됩니다. 이 기능은 단순히 맵 함수에 전달되는 증분 값입니다.
Lee Campbell

설명서에 대한 링크가 손상되었습니다.
Usman Mutawakil

3

https://github.com/poetix/protonpack을 사용하면 그 우편 번호를 할 수 있습니다.

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = IntStream.range(0, names.length).boxed(); 

nameList = StreamUtils.zip(indices, stream(names),SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey()).map(Entry::getValue).collect(toList());                   

System.out.println(nameList);

3

타사 라이브러리를 사용하여 괜찮다면, 이클립스 컬렉션zipWithIndexforEachWithIndex많은 종류에서 사용할 수. 다음은 JDK 유형과 Eclipse Collections 유형을 모두 사용하여이 과제를 해결하는 일련의 솔루션 zipWithIndex입니다.

String[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
ImmutableList<String> expected = Lists.immutable.with("Erik");
Predicate<Pair<String, Integer>> predicate =
    pair -> pair.getOne().length() <= pair.getTwo() + 1;

// JDK Types
List<String> strings1 = ArrayIterate.zipWithIndex(names)
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings1);

List<String> list = Arrays.asList(names);
List<String> strings2 = ListAdapter.adapt(list)
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings2);

// Eclipse Collections types
MutableList<String> mutableNames = Lists.mutable.with(names);
MutableList<String> strings3 = mutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings3);

ImmutableList<String> immutableNames = Lists.immutable.with(names);
ImmutableList<String> strings4 = immutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings4);

MutableList<String> strings5 = mutableNames.asLazy()
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne, Lists.mutable.empty());
Assert.assertEquals(expected, strings5);

forEachWithIndex대신 사용하는 솔루션이 있습니다.

MutableList<String> mutableNames =
    Lists.mutable.with("Sam", "Pamela", "Dave", "Pascal", "Erik");
ImmutableList<String> expected = Lists.immutable.with("Erik");

List<String> actual = Lists.mutable.empty();
mutableNames.forEachWithIndex((name, index) -> {
        if (name.length() <= index + 1)
            actual.add(name);
    });
Assert.assertEquals(expected, actual);

람다를 익명의 내부 클래스로 변경하면 이러한 모든 코드 예제가 Java 5-7에서도 작동합니다.

참고 : 저는 Eclipse Collections의 커미터입니다.


2

Vavr (이전의 Javaslang)을 사용하는 경우 전용 방법을 활용할 수 있습니다.

Stream.of("A", "B", "C")
  .zipWithIndex();

내용을 인쇄하면 흥미로운 내용이 나타납니다.

Stream((A, 0), ?)

이것은 Streams게으르고 스트림의 다음 항목에 대한 실마리 가 없기 때문 입니다.


1

술어를 기반으로 색인을 얻으려고 시도하는 경우 다음을 시도하십시오.

첫 번째 색인에만 관심이있는 경우 :

OptionalInt index = IntStream.range(0, list.size())
    .filter(i -> list.get(i) == 3)
    .findFirst();

또는 여러 색인을 찾으려면 다음을 수행하십시오.

IntStream.range(0, list.size())
   .filter(i -> list.get(i) == 3)
   .collect(Collectors.toList());

.orElse(-1);값을 찾지 못하면 값을 반환하려는 경우 추가 하십시오.


1

다음은 AbacusUtil의 코드입니다 .

Stream.of(names).indexed()
      .filter(e -> e.value().length() <= e.index())
      .map(Indexed::value).toList();

공개 : 저는 AbacusUtil의 개발자입니다.


1

IntStream.iterate()색인을 얻는 데 사용할 수 있습니다 .

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i < names.length, i -> i + 1)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

이것은 Java 8 이상의 Java 9에서만 작동합니다.

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i + 1)
        .limit(names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

0

아래 예제에서 필요한 것처럼 인덱서를 캡슐화하는 정적 내부 클래스를 만들 수 있습니다.

static class Indexer {
    int i = 0;
}

public static String getRegex() {
    EnumSet<MeasureUnit> range = EnumSet.allOf(MeasureUnit.class);
    StringBuilder sb = new StringBuilder();
    Indexer indexer = new Indexer();
    range.stream().forEach(
            measureUnit -> {
                sb.append(measureUnit.acronym);
                if (indexer.i < range.size() - 1)
                    sb.append("|");

                indexer.i++;
            }
    );
    return sb.toString();
}

0

이 질문 ( boolean과 일치하는 첫 번째 요소의 색인을 얻는 Stream Way )은 현재 질문을 중복으로 표시 했으므로 거기에서 대답 할 수 없습니다. 나는 여기에 대답하고 있습니다.

다음은 외부 라이브러리가 필요하지 않은 일치하는 인덱스를 얻는 일반적인 솔루션입니다.

당신이 목록이 있다면.

public static <T> int indexOf(List<T> items, Predicate<T> matches) {
        return IntStream.range(0, items.size())
                .filter(index -> matches.test(items.get(index)))
                .findFirst().orElse(-1);
}

그리고 이것을 다음과 같이 부르십시오.

int index = indexOf(myList, item->item.getId()==100);

컬렉션을 사용하는 경우이 방법을 사용해보십시오.

   public static <T> int indexOf(Collection<T> items, Predicate<T> matches) {
        int index = -1;
        Iterator<T> it = items.iterator();
        while (it.hasNext()) {
            index++;
            if (matches.test(it.next())) {
                return index;
            }
        }
        return -1;
    }

0

가능한 한 가지 방법은 플로우에서 각 요소를 색인화하는 것입니다.

AtomicInteger index = new AtomicInteger();
Stream.of(names)
  .map(e->new Object() { String n=e; public i=index.getAndIncrement(); })
  .filter(o->o.n.length()<=o.i) // or do whatever you want with pairs...
  .forEach(o->System.out.println("idx:"+o.i+" nam:"+o.n));

스트림을 따라 익명 클래스를 사용하는 것은 매우 유용하지만 잘 사용되지 않습니다.


0

map 반드시
LINQ 예제와 가장 가까운 람다 일 필요 는 없습니다 .

int[] idx = new int[] { 0 };
Stream.of( names ).filter( name -> name.length() <= idx[0]++ ).collect( Collectors.toList() );

0
String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
String completeString
         =  IntStream.range(0,namesArray.length)
           .mapToObj(i -> namesArray[i]) // Converting each array element into Object
           .map(String::valueOf) // Converting object to String again
           .collect(Collectors.joining(",")); // getting a Concat String of all values
        System.out.println(completeString);

출력 : Sam, Pamela, Dave, Pascal, Erik

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

IntStream.range(0,namesArray.length)
               .mapToObj(i -> namesArray[i]) // Converting each array element into Object
               .map(String::valueOf) // Converting object to String again
               .forEach(s -> {
                //You can do various operation on each element here
                System.out.println(s);
               }); // getting a Concat String of all 

목록에서 수집하려면 :

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
 List<String> namesList
                =  IntStream.range(0,namesArray.length)
                .mapToObj(i -> namesArray[i]) // Converting each array element into Object
                .map(String::valueOf) // Converting object to String again
                .collect(Collectors.toList()); // collecting elements in List
        System.out.println(listWithIndex);

위의 질문에 대한 해결책은 List하나의 요소 Erik을 포함하는 것으로 예상됩니다 .
Kaplan

List에서도 수집하는 예제를 추가했습니다.
Arpan Saini

0

jean-baptiste-yunès가 말했듯이 스트림이 Java List를 기반으로하는 경우 AtomicInteger 및 그것의 incrementAndGet 메소드를 사용하면 문제에 대한 매우 좋은 해결책이며 반환 된 정수는 원래 List의 색인에 해당하는 한 병렬 스트림을 사용하지 마십시오.


0

forEach에 색인이 필요한 경우 방법을 제공합니다.

  public class IndexedValue {

    private final int    index;
    private final Object value;

    public IndexedValue(final int index, final Object value) { 
        this.index = index;
        this.value = value;
    }

    public int getIndex() {
        return index;
    }

    public Object getValue() {
        return value;
    }
}

그런 다음 다음과 같이 사용하십시오.

@Test
public void withIndex() {
    final List<String> list = Arrays.asList("a", "b");
    IntStream.range(0, list.size())
             .mapToObj(index -> new IndexedValue(index, list.get(index)))
             .forEach(indexValue -> {
                 System.out.println(String.format("%d, %s",
                                                  indexValue.getIndex(),
                                                  indexValue.getValue().toString()));
             });
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.