从流中收集连续对


102

给定流如 { 0, 1, 2, 3, 4 }

如何最优雅地将其转换为给定形式:

{ new Pair(0, 1), new Pair(1, 2), new Pair(2, 3), new Pair(3, 4) }

(当然,假设我已经定义了Pair类)?

编辑:这不完全是关于整数或原始流。对于任何类型的流,答案应该是通用的。


2
FP中的术语是“分区”,但是我找不到Java中具有所需语义的方法。它对谓词进行分区。
Marko Topolnik 2013年

1
通常,JDK 8中的分隔符被认为是用于遍历和分区的目的。我也会尝试举一个例子。
Olimpiu POP

list.stream().map(i -> new Pair(i, i+1));
aepurniet

2
对于等效的非流问题,请stackoverflow.com/questions/17453022/...
Raedwald

顺便说一句,有些人使用Map.EntryPair类的任何一种实现。(当然,有些人可能会认为这是黑客,但使用内置的类很方便。)
Basil Bourque

Answers:


33

我的StreamEx库扩展了标准流,它pairMap为所有流类型提供了一种方法。对于原始流,它不会更改流类型,但可用于进行一些计算。最常见的用法是计算差异:

int[] pairwiseDiffs = IntStreamEx.of(input).pairMap((a, b) -> (b-a)).toArray();

对于对象流,您可以创建任何其他对象类型。我的库没有提供任何新的用户可见的数据结构Pair(这是库概念的一部分)。但是,如果您有自己的Pair类并想使用它,则可以执行以下操作:

Stream<Pair> pairs = IntStreamEx.of(input).boxed().pairMap(Pair::new);

或者,如果您已经有一些Stream

Stream<Pair> pairs = StreamEx.of(stream).pairMap(Pair::new);

此功能是使用自定义分隔符实现的。它的开销很低,可以很好地并行化。当然,它可以与任何流源一起使用,而不仅仅是像许多其他解决方案一样随机访问列表/数组。在许多测试中,它的表现都非常好。这是一个JMH基准,我们可以使用不同的方法找到所有输入值在较大值之前的值(请参阅问题)。


谢谢!我对这个图书馆的研究越多,我就越喜欢它。我可能最终会开始使用流。(StreamEx工具Iterable!Hurrah!)
Aleksandr Dubinsky

为了使您的答案100%完整,您能否说明如何将a Stream换成a StreamEx
Aleksandr Dubinsky

3
@AleksandrDubinsky:只需使用StreamEx.of(stream)。还有其他方便的静态方法可以从Collection,数组Reader等创建流。编辑了答案。
塔吉尔·瓦列夫

@TagirValeev是pairMap在顺序流上订购的吗?实际上,我想拥有forPairsOrdered(),但是由于没有这种方法,我可以以某种方式对其进行仿真吗?stream.ordered().forPairs()还是stream().pairMap().forEachOrdered()
Askar Kalykov

1
@AskarKalykov,pairMap是具有无干扰无状态映射器功能的中间操作,未按与simple相同的方式为其指定顺序mapforPairs规范未对进行排序,但对顺序流按顺序对无序操作进行了事实排序。如果将原始问题公式化为单独的stackoverflow问题以提供更多上下文,那将是很好的。
塔吉尔·瓦列夫(Tagir Valeev)

74

Java 8流库主要用于将流分成较小的块以进行并行处理,因此有状态管道阶段非常有限,并且不支持执行诸如获取当前流元素的索引和访问相邻流元素之类的操作。

解决这些问题的一种典型方法(当然有一些限制)是通过索引来驱动流,并依赖于在某些随机访问数据结构(例如ArrayList)中处理值,可以从中检索元素。如果值为in arrayList,则可以通过执行以下操作来生成请求的对:

    IntStream.range(1, arrayList.size())
             .mapToObj(i -> new Pair(arrayList.get(i-1), arrayList.get(i)))
             .forEach(System.out::println);

当然,限制是输入不能是无限流。但是,该管道可以并行运行。


5
“输入不能是无限流。” 实际上,输入根本不是流。输入(arrayList)实际上是一个集合,这就是为什么我没有将其标记为答案。(但恭喜您获得了金牌!)
Aleksandr Dubinsky

16

这不是很优雅,这是一个棘手的解决方案,但适用于无限流

Stream<Pair> pairStream = Stream.iterate(0, (i) -> i + 1).map( // natural numbers
    new Function<Integer, Pair>() {
        Integer previous;

        @Override
        public Pair apply(Integer integer) {
            Pair pair = null;
            if (previous != null) pair = new Pair(previous, integer);
            previous = integer;
            return pair;
        }
    }).skip(1); // drop first null

现在,您可以将流限制为所需的长度

pairStream.limit(1_000_000).forEach(i -> System.out.println(i));

PS我希望有更好的解决方案,例如clojure(partition 2 1 stream)


6
指出匿名类有时是lambda的有用替代品。
Aleksandr Dubinsky 2013年

2
@aepurniet我认为它将无法正常工作。根据parallelStream文档:“为了保持正确的行为,这些行为参数必须是无干扰的,并且在大多数情况下必须是无状态的”
mishadoff 2013年

14
这完全与流框架的设计相反,并且由于匿名函数不是无状态的,因此直接违反了map API的约定。尝试使用并行流和更多数据运行它,以便流框架创建更多工作线程,您将看到结果:很少有随机“错误”,几乎不可能再现,并且很难检测,直到有足够的数据(在生产中?)。这可能是灾难性的。
马里奥·罗西

4
@AleksandrDubinsky您对极限/跳过可并行化是不正确的;实际上,JDK中提供的实现可以并行工作。因为操作是与遇到顺序联系在一起的,所以并行化可能并不总是提供性能优势,但是在高Q情况下,并行化可以提供。
Brian Goetz

4
@AleksandrDubinsky不正确。如果流是它可能会跳过一个随机元素无序(没有定义的相遇顺序,因此在逻辑上有没有“第一”或“第n个”元素,只是元素。),但流是否有序或无序,跳过始终能并行工作。如果流是有序的,则提取的并行性要少一些,但仍是并行的。
Brian Goetz

15

我实现了一个拆分器包装器,该包装器将原始拆分器中的每个n元素都包含T在内并产生List<T>

public class ConsecutiveSpliterator<T> implements Spliterator<List<T>> {

    private final Spliterator<T> wrappedSpliterator;

    private final int n;

    private final Deque<T> deque;

    private final Consumer<T> dequeConsumer;

    public ConsecutiveSpliterator(Spliterator<T> wrappedSpliterator, int n) {
        this.wrappedSpliterator = wrappedSpliterator;
        this.n = n;
        this.deque = new ArrayDeque<>();
        this.dequeConsumer = deque::addLast;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<T>> action) {
        deque.pollFirst();
        fillDeque();
        if (deque.size() == n) {
            List<T> list = new ArrayList<>(deque);
            action.accept(list);
            return true;
        } else {
            return false;
        }
    }

    private void fillDeque() {
        while (deque.size() < n && wrappedSpliterator.tryAdvance(dequeConsumer))
            ;
    }

    @Override
    public Spliterator<List<T>> trySplit() {
        return null;
    }

    @Override
    public long estimateSize() {
        return wrappedSpliterator.estimateSize();
    }

    @Override
    public int characteristics() {
        return wrappedSpliterator.characteristics();
    }
}

可以使用以下方法创建连续流:

public <E> Stream<List<E>> consecutiveStream(Stream<E> stream, int n) {
    Spliterator<E> spliterator = stream.spliterator();
    Spliterator<List<E>> wrapper = new ConsecutiveSpliterator<>(spliterator, n);
    return StreamSupport.stream(wrapper, false);
}

用法示例:

consecutiveStream(Stream.of(0, 1, 2, 3, 4, 5), 2)
    .map(list -> new Pair(list.get(0), list.get(1)))
    .forEach(System.out::println);

每个元素重复两次吗?
Aleksandr Dubinsky

不。它创建一个包含List<E>元素的新流。每个列表包含n原始流中的连续元素。自己检查一下;)
TomekRękawek2013年

您能否修改答案,以便重复每个元素(第一个和最后一个除外)?
Aleksandr Dubinsky 2013年

4
+1我认为这是一项很好的工作,除了分区大小外,还应该推广到任何步长。非常需要一个(partition size step)函数,这是获得它的最佳方法。
Marko Topolnik 2014年

3
考虑ArrayDeque优先使用来提高性能LinkedList
Marko Topolnik 2014年

14

您可以使用Stream.reduce()方法执行此操作(使用此技术我没有看到其他答案)。

public static <T> List<Pair<T, T>> consecutive(List<T> list) {
    List<Pair<T, T>> pairs = new LinkedList<>();
    list.stream().reduce((a, b) -> {
        pairs.add(new Pair<>(a, b));
        return b;
    });
    return pairs;
}

1
它会返回(1,2)(2,3)而不是(1,2)(3,4)。另外,我不确定是否会按顺序应用它(肯定不能保证这样做)。
Aleksandr Dubinsky

1
请检查问题,这是预期的行为@Aleksandr Dubinsky
SamTebbs33

3
啊,是的,抱歉。想一想,我写了它。
Aleksandr Dubinsky


6

您可以使用滑动运算符在cyclops-react(我为该库提供)中进行此操作。

  LazyFutureStream.of( 0, 1, 2, 3, 4 )
                  .sliding(2)
                  .map(Pair::new);

要么

   ReactiveSeq.of( 0, 1, 2, 3, 4 )
                  .sliding(2)
                  .map(Pair::new);

假设Pair构造函数可以接受带有2个元素的Collection。

如果要按4分组,也可以按2递增。

     ReactiveSeq.rangeLong( 0L,Long.MAX_VALUE)
                .sliding(4,2)
                .forEach(System.out::println);

在cyclops-streams StreamUtils类中还提供了用于在java.util.stream.Stream上创建滑动视图的等效静态方法。

       StreamUtils.sliding(Stream.of(1,2,3,4),2)
                  .map(Pair::new);

注意:-对于单线程操作,ReactiveSeq会更合适。LazyFutureStream扩展了ReactiveSeq,但主要用于并行/并行使用(它是期货流)。

LazyFutureStream扩展了ReactiveSeq,后者从了不起的jOOλ(扩展了java.util.stream.Stream)扩展了Seq,因此Lukas提出的解决方案也适用于两种Stream类型。对于感兴趣的任何人,窗口/滑动运算符之间的主要区别是明显的相对功能/复杂度之间的折衷以及适用于无限流的适用性(滑动不会消耗流,而是在流流动时进行缓冲)。


这样,您获得[[0,1)(2,3)...],但问题询问[[0,1)(1,2)...]。请使用RxJava查看我的答案...
frhack

1
您是对的,我的错,我误解了问题-滑动运算符是在此使用的正确运算符。我将更新我的答案-谢谢!
John McClean

4

质子包库提供了窗口functionnality。给定一个Pair类和一个Stream,您可以这样做:

Stream<Integer> st = Stream.iterate(0 , x -> x + 1);
Stream<Pair<Integer, Integer>> pairs = StreamUtils.windowed(st, 2, 1)
                                                  .map(l -> new Pair<>(l.get(0), l.get(1)))
                                                  .moreStreamOps(...);

现在pairs流包含:

(0, 1)
(1, 2)
(2, 3)
(3, 4)
(4, ...) and so on

但是,看来您需要创建st两次!该库可以使用单个流解决问题吗?
Aleksandr Dubinsky,2015年

@AleksandrDubinsky我不认为,因此当前拆分器可用。我提出一个问题github.com/poetix/protonpack/issues/9
亚历克西斯C.

@AleksandrDubinsky windowed已添加功能!参见编辑。
Alexis C.

1
为什么不删除旧答案,以便其他用户可以看到解决方案,而不是历史记录。
Aleksandr Dubinsky 2015年

4

寻找连续对

如果您愿意使用第三方库并且不需要并行性,那么jOOλ提供如下的SQL样式窗口函数

System.out.println(
Seq.of(0, 1, 2, 3, 4)
   .window()
   .filter(w -> w.lead().isPresent())
   .map(w -> tuple(w.value(), w.lead().get())) // alternatively, use your new Pair() class
   .toList()
);

屈服

[(0, 1), (1, 2), (2, 3), (3, 4)]

lead()函数从窗口中按遍历顺序访问下一个值。

查找连续的三元组/四元组/ n元组

评论中的一个问题是寻求一个更通用的解决方案,在该解决方案中,不应该收集对,而应该收集n元组(或可能的列表)。因此,这是另一种方法:

int n = 3;

System.out.println(
Seq.of(0, 1, 2, 3, 4)
   .window(0, n - 1)
   .filter(w -> w.count() == n)
   .map(w -> w.window().toList())
   .toList()
);

产生清单清单

[[0, 1, 2], [1, 2, 3], [2, 3, 4]]

没有filter(w -> w.count() == n),结果将是

[[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4], [4]]

免责声明:我在jOOλ背后的公司工作


有趣。如果我需要对3个或更多元素进行分组怎么办?使用w.lead().lead()
Raul Santelices

1
@RaulSantelices:tuple(w.value(), w.lead(1), w.lead(2))将是一个选择。我已经为length = n
卢卡斯·埃德

1
我正确理解,这.window()不是将整个输入流收集到某个中间集合中然后从中创建新流的惰性操作吗?
塔吉尔·瓦列夫

@TagirValeev:是的,这是当前的实现。在上述情况下(不Comparator用于重新排序窗口),然后像一个优化将是可能的,而且很可能在将来实现。
卢卡斯·艾德


2

我们可以使用RxJava(非常强大的反应式扩展库)

IntStream intStream  = IntStream.iterate(1, n -> n + 1);

Observable<List<Integer>> pairObservable = Observable.from(intStream::iterator).buffer(2,1);

pairObservable.take(10).forEach(b -> {
            b.forEach(n -> System.out.println(n));
            System.out.println();
        });

所述缓冲 操作者变换可观察到的发射项目到可观察到的发射缓冲那些项的集合..


1
我用光Observable.zip(obs, obs.skip(1), pair->{...})了直到现在!我不知道Observable.buffer有一个带有步骤的版本(而且我已经习惯了zippython 的技巧)。+1
Reut Sharabani

1

该操作本质上是有状态的,因此并不是真正意味着要解决什么流-请参阅javadoc中的“无状态行为”部分:

最好的方法是避免使用有状态的行为参数来完全传输操作

这里的一种解决方案是通过外部计数器在流中引入状态,尽管它仅适用于顺序流。

public static void main(String[] args) {
    Stream<String> strings = Stream.of("a", "b", "c", "c");
    AtomicReference<String> previous = new AtomicReference<>();
    List<Pair> collect = strings.map(n -> {
                            String p = previous.getAndSet(n);
                            return p == null ? null : new Pair(p, n);
                        })
                        .filter(p -> p != null)
                        .collect(toList());
    System.out.println(collect);
}


static class Pair<T> {
    private T left, right;
    Pair(T left, T right) { this.left = left; this.right = right; }
    @Override public String toString() { return "{" + left + "," + right + '}'; }
}

这个问题要求收集输入流的连续元素,而不仅仅是收集连续的整数。重要的术语说明:Stream!=“ lambdas”。
Aleksandr Dubinsky

您可以将AtomicInteger替换为AtomicReference。另一种选择是滚动自己的收集器或使用外部库,例如在此示例中:stackoverflow.com/a/30090528/829571
assylias 2015年

看到我的编辑。另外,我不确定我是否理解您对lambda!=流的评论。使用匿名类的另一个答案在本质上是一样的,除了状态由匿名类而不是外部持有...
assylias 2015年

1
这样可行。该StreamEx图书馆也是一个很好的发现,可能是本身就是一个答案。我对“ streams!= lambdas”的评论是指您说“该操作本质上是有状态的,因此,lambdas并不是要解决的问题”。我认为您的意思是使用“流”一词。
Aleksandr Dubinsky

哦,我明白了-我已经澄清了。
亚述2015年

0

在您的情况下,我将编写我的自定义IntFunction,以跟踪最后传递的int并将其用于映射原始IntStream。

import java.util.function.IntFunction;
import java.util.stream.IntStream;

public class PairFunction implements IntFunction<PairFunction.Pair> {

  public static class Pair {

    private final int first;
    private final int second;

    public Pair(int first, int second) {
      this.first = first;
      this.second = second;
    }

    @Override
    public String toString() {
      return "[" + first + "|" + second + "]";
    }
  }

  private int last;
  private boolean first = true;

  @Override
  public Pair apply(int value) {
    Pair pair = !first ? new Pair(last, value) : null;
    last = value;
    first = false;
    return pair;
  }

  public static void main(String[] args) {

    IntStream intStream = IntStream.of(0, 1, 2, 3, 4);
    final PairFunction pairFunction = new PairFunction();
    intStream.mapToObj(pairFunction)
        .filter(p -> p != null) // filter out the null
        .forEach(System.out::println); // display each Pair

  }

}

问题在于它会将无状态性抛出窗口。
罗布2013年

@Rob,这是什么问题?
Aleksandr Dubinsky

Lambda的要点之一是不具有可变状态,以便内部集成商可以并行处理工作。
罗布2013年

@Rob:是的,您是对的,但是给定的示例流无论如何都无法并行化,因为每一项(第一项和最后一项除外)都被用作某对的第一项和第二项。
jpvee 2013年

@jpvee是的,我认为这就是您的想法。我想知道是否没有办法用其他映射器来做到这一点。本质上,您所需要做的就是使循环增量器加二,然后让函子接受2个参数。那一定是可能的。
罗布

0

为了计算时间序列的时间(x值)的连续差异,我使用streamcollect(...)方法:

final List< Long > intervals = timeSeries.data().stream()
                    .map( TimeSeries.Datum::x )
                    .collect( DifferenceCollector::new, DifferenceCollector::accept, DifferenceCollector::combine )
                    .intervals();

其中DifferenceCollector是这样的:

public class DifferenceCollector implements LongConsumer
{
    private final List< Long > intervals = new ArrayList<>();
    private Long lastTime;

    @Override
    public void accept( final long time )
    {
        if( Objects.isNull( lastTime ) )
        {
            lastTime = time;
        }
        else
        {
            intervals.add( time - lastTime );
            lastTime = time;
        }
    }

    public void combine( final DifferenceCollector other )
    {
        intervals.addAll( other.intervals );
        lastTime = other.lastTime;
    }

    public List< Long > intervals()
    {
        return intervals;
    }
}

您可以修改此设置以满足您的需求。


0

我终于找到了一种诱骗Stream.reduce的方法,使其能够整齐地处理成对的值。有许多需要这种功能的用例在JDK 8 中并不自然出现:

public static int ArithGeo(int[] arr) {
    //Geometric
    List<Integer> diffList = new ArrayList<>();
    List<Integer> divList = new ArrayList<>();
    Arrays.stream(arr).reduce((left, right) -> {
        diffList.add(right-left);
        divList.add(right/left);
        return right;
    });
    //Arithmetic
    if(diffList.stream().distinct().count() == 1) {
        return 1;
    }
    //Geometric
    if(divList.stream().distinct().count() == 1) {
        return 2;
    }
    return -1;
}

我使用的技巧是返回权;声明。


1
我认为无法reduce为这项工作提供足够的保证。
Aleksandr Dubinsky

有兴趣了解更多有关充分保证的信息。你能详细说明一下吗?番石榴也许还有其他选择...但是我受到限制,无法使用它。
Beezer

-1

一个优雅的解决方案是使用zip。就像是:

List<Integer> input = Arrays.asList(0, 1, 2, 3, 4);
Stream<Pair> pairStream = Streams.zip(input.stream(),
                                      input.stream().substream(1),
                                      (a, b) -> new Pair(a, b)
);

这非常简洁,优雅,但是它使用列表作为输入。无限流源无法通过这种方式进行处理。

另一个(更麻烦的是)问题是,最近将zip和整个Streams类一起从API中删除了。上面的代码仅适用于b95或更早的版本。因此,对于最新的JDK,我会说没有优雅的FP样式解决方案,现在我们可以希望以某种方式将zip重新引入API。


确实,zip被删除了。我不记得所有关于Streams类的内容,但是有些东西已经迁移为Stream接口上的静态方法,并且还有StreamSupportStream.Builder类。
Stuart Marks

那就对了。其他一些方法,例如concat或iterate,已被移动并成为Stream中的默认方法。遗憾的是,刚刚从API中删除了zip。我了解这种选择背后的原因(例如,缺少元组),但这仍然是一个不错的功能。
小工具

2
@gadget元组有什么关系zip?无论发明什么愚蠢的理由都不能证明杀人是有道理的zip
亚历山大·杜宾斯基

@AleksandrDubinsky在大多数情况下,zip用于产生成对/成对输出的集合。他们争辩说,如果保留邮编,人们也会要求JDK包含Tuples。我永远不会删除现有功能。
小工具

-1

这是一个有趣的问题。我的混合尝试下面有什么好处吗?

public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3);
    Iterator<Integer> first = list.iterator();
    first.next();
    if (first.hasNext())
        list.stream()
        .skip(1)
        .map(v -> new Pair(first.next(), v))
        .forEach(System.out::println);
}

我认为它不适合并行处理,因此可能会被取消资格。


这个问题并没有要求并行处理,但是它确实假设我们只有一个Stream,没有一个List。当然,我们也可以从Stream中撬起迭代器,因此这可能是有效的解决方案。但是,这是一种原始方法。
Aleksandr Dubinsky

-1

正如其他人所观察到的,由于问题的性质,需要有一定的状态。

我遇到了一个类似的问题,在这个问题中,我想要的实际上是Oracle SQL函数LEAD。我的实现尝试如下。

/**
 * Stream that pairs each element in the stream with the next subsequent element.
 * The final pair will have only the first item, the second will be null.
 */
<T> Spliterator<Pair<T>> lead(final Stream<T> stream)
{
    final Iterator<T> input = stream.sequential().iterator();

    final Iterable<Pair<T>> iterable = () ->
    {
        return new Iterator<Pair<T>>()
        {
            Optional<T> current = getOptionalNext(input);

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

            @Override
            public Pair<T> next()
            {
                Optional<T> next = getOptionalNext(input);
                final Pair<T> pair = next.isPresent()
                    ? new Pair(current.get(), next.get())
                    : new Pair(current.get(), null);
                current = next;

                return pair;
            }
        };
    };

    return iterable.spliterator();
}

private <T> Optional<T> getOptionalNext(final Iterator<T> iterator)
{
    return iterator.hasNext()
        ? Optional.of(iterator.next())
        : Optional.empty();
}

-1

您可以通过使用有界队列来存储流经流的元素来实现这一点(基于我在这里详细描述的思想:是否可以在Stream中获取下一个元素?

下面的示例首先定义BoundedQueue类的实例,该实例将存储流中的元素(如果您不喜欢扩展LinkedList的想法,请参考上面提到的链接以获取替代的和更通用的方法)。稍后,您只需将两个后续元素组合到Pair实例中:

public class TwoSubsequentElems {
  public static void main(String[] args) {
    List<Integer> input = new ArrayList<Integer>(asList(0, 1, 2, 3, 4));

    class BoundedQueue<T> extends LinkedList<T> {
      public BoundedQueue<T> save(T curElem) {
        if (size() == 2) { // we need to know only two subsequent elements
          pollLast(); // remove last to keep only requested number of elements
        }

        offerFirst(curElem);

        return this;
      }

      public T getPrevious() {
        return (size() < 2) ? null : getLast();
      }

      public T getCurrent() {
        return (size() == 0) ? null : getFirst();
      }
    }

    BoundedQueue<Integer> streamHistory = new BoundedQueue<Integer>();

    final List<Pair<Integer>> answer = input.stream()
      .map(i -> streamHistory.save(i))
      .filter(e -> e.getPrevious() != null)
      .map(e -> new Pair<Integer>(e.getPrevious(), e.getCurrent()))
      .collect(Collectors.toList());

    answer.forEach(System.out::println);
  }
}

-3

我同意@aepurniet,但必须使用mapToObj来代替map

range(0, 100).mapToObj((i) -> new Pair(i, i+1)).forEach(System.out::println);

1
对。但这只是收集整数对,而不是流(任何类型)的元素对。
Aleksandr Dubinsky

-5

运行for从0到length-1流的循环

for(int i = 0 ; i < stream.length-1 ; i++)
{
    Pair pair = new Pair(stream[i], stream[i+1]);
    // then add your pair to an array
}

3
该解决方案的lambda部分在哪里?
Olimpiu POP

这是不是这样的,当流是无限的
mishadoff

@Olimpiu-在哪里需要lambda?我阅读了两次该问题,以确保我没有错过它。我还检查了编辑的历史记录。而且这个问题没有加标签。
jww
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.