Przeczytaj n losowych linii z potencjalnie dużego pliku


16

Wyzwanie dotyczy odczytu losowych linii z potencjalnie dużego pliku bez wczytywania całego pliku do pamięci.

Wejście

Liczba całkowita ni nazwa pliku tekstowego.

Wynik

n wiersze pliku tekstowego wybrane losowo równomiernie bez zamiany.

Możesz założyć, że nmieści się w zakresie od 1 do liczby linii w pliku.

Zachowaj ostrożność, próbkując nlosowo liczby z zakresu, w którym odpowiedź jest jednorodna. rand()%nna przykład w C nie jest jednolity. Każdy wynik musi być równie prawdopodobny.

Zasady i ograniczenia

Każda linia pliku tekstowego będzie miała tę samą liczbę znaków i nie będzie więcej niż 80.

Twój kod nie może czytać żadnej zawartości pliku tekstowego, z wyjątkiem:

  • Linie, które wyprowadza.
  • Pierwszy wiersz, aby sprawdzić, ile znaków w wierszu znajduje się w pliku tekstowym.

Możemy założyć, że każdy znak w pliku tekstowym zajmuje dokładnie jeden bajt.

Zakłada się, że separatory linii mają długość 1 bajta. Rozwiązania mogą korzystać z 2-bajtowych separatorów długich linii tylko wtedy, gdy określają to. Możesz również założyć, że ostatnia linia jest zakończona separatorem linii.

Twoja odpowiedź powinna być kompletnym programem, ale możesz podać dane wejściowe w dowolny dogodny sposób.

Języki i biblioteki

Możesz użyć dowolnego języka lub biblioteki.

Notatki

Wystąpił problem z obliczeniem liczby linii w pliku. Jak oni wskazują w komentarzach, możesz wywnioskować to na podstawie rozmiaru pliku i liczby znaków w wierszu.

Motywacja

Na czacie niektórzy pytali, czy to naprawdę pytanie „Wykonaj X bez Y”. Tłumaczę to, aby zapytać, czy ograniczenia są niezwykle sztuczne.

Zadanie losowego próbkowania linii z dużych plików nie jest rzadkie i w rzeczywistości muszę je czasem wykonać. Jednym ze sposobów na to jest bash:

shuf -n <num-lines>

Jest to jednak bardzo wolne w przypadku dużych plików, ponieważ odczytuje się w całym pliku.


Dlaczego głosowanie negatywne?

3
Jest to banalne w językach takich jak C fseek, a w innych niemożliwe. Ponadto, co jeśli njest większa niż liczba linii w pliku?
— Mego

4
@Mego: w odniesieniu do punktu b): możesz obliczyć liczbę linii, dzieląc rozmiar pliku przez długość linii.
— nimi

8
Czy X bez Y jest ostrzeżeniem rozpoczynającym się od „To nie zawsze jest złe”. Głównym problemem są sztuczne ograniczenia, takie jak „nie używaj +”, co daje przewagę językom, które mają sum(). Brak wczytywania pliku do pamięci jest wyraźnym i spójnym ograniczeniem, które nie jest w żaden sposób arbitralne. Można go przetestować z plikiem większym niż pamięć, którego nie można obejść z powodu różnic językowych. Zdarza się również, że ma zastosowania w świecie rzeczywistym (chociaż nie jest to konieczne w przypadku golfa ...).
— trichoplax

1
Wygląda na to, że jest to właściwie golf o ograniczonej złożoności, w którym użycie pamięci jest ograniczone pomimo potencjalnie dużych plików. Nie chodzi o to, by nie zawierać pewnych elementów w kodzie, ale ograniczenie jego działania.
— xnor

Odpowiedzi:


5

Dyalog APL , 63 bajty

⎕NREAD¨t 82l∘,¨lׯ1+⎕?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍞⎕NTIE 0

Monituje o nazwę pliku, a następnie o liczbę pożądanych linii losowych.

Wyjaśnienie

⍞Monituj o wprowadzenie tekstu (nazwa pliku)
⎕NTIE 0Przywiąż plik, używając następnego dostępnego numeru remisu (-1 w czystym systemie)
t←Zapisz wybrany numer remisu jako t
83 80,⍨Dołącz [83,80], uzyskując [-1,83,80]
⎕NREADPrzeczytaj pierwsze 80 bajtów pliku -1 jako liczby całkowite 8-bitowe (kod konwersji 83)
10⍳⍨Znajdź indeks pierwszej liczby 10 (LF)
l←Zapisz długość linii jako l
(⎕NSIZE t)÷Podziel rozmiar pliku -1 za pomocą długości linii
⎕Monit o wprowadzenie danych liczbowych (żądana liczba linii )
?X losowych wyborów (bez zamiany) pierwszych Y liczb naturalnych Y
¯1+Dodaj -1, aby uzyskać numery linii 0-początkowe *
l×Pomnóż przez długość linii, aby uzyskać bajty początkowe
t 82l∘,¨Przyłóż [-1,82, Długość linii] do każdego bajtu początkowego (tworzy lista argumentów za ⎕NREAD)
⎕NREAD¨ Czytaj każdy wiersz jako znak 8-bitowy (kod konwersji 82)

Praktyczny przykład

Plik /tmp/records.txt zawiera:

Hello
Think
12345
Klaus
Nilad

Spraw, aby program RandLines zawierał powyższy kod dosłownie, wprowadzając następujące dane do sesji APL:

∇RandLines
⎕NREAD¨t 82l∘,¨lׯ1+⎕?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍞⎕NTIE 0
∇

W typie sesji APL RandLinesnaciśnij klawisz Enter.

System przesuwa kursor do następnego wiersza, który jest pytaniem o długość 0 dla danych znakowych; wchodzić /tmp/records.txt.

System wysyła teraz dane wyjściowe ⎕:i oczekuje na wprowadzenie numeryczne; wchodzić 4.

System generuje cztery losowe linie.

Prawdziwe życie

W rzeczywistości możesz podać nazwę pliku i policzyć jako argumenty, a wynik otrzymać w postaci tabeli. Można to zrobić, wprowadzając:

RandLs←{↑⎕NREAD¨t 82l∘,¨lׯ1+⍺?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍵⎕NTIE 0}

Teraz sprawisz, że MyLines zawiera trzy losowe linie z:

MyLines←3 RandLs'/tmp/records.txt'

A może zwrócisz tylko jedną losową linię, jeśli nie określono liczby:

RandL←{⍺←1 ⋄ ↑⎕NREAD¨t 82l∘,¨lׯ1+⍺?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍵⎕NTIE 0}

Teraz możesz zrobić oba:

MyLines←2 RandL'/tmp/records.txt'

oraz (zauważ brak lewego argumentu):

MyLine←RandL'/tmp/records.txt'

Czytanie kodu

Jednoliniowe golfa APL to zły pomysł. Oto jak napisałbym w systemie produkcyjnym:

RandL←{ ⍝ Read X random lines from file Y without reading entire file
    ⍺←1 ⍝ default count
    tie←⍵⎕NTIE 0 ⍝ tie file
    length←10⍳⍨⎕NREAD 83 80,⍨tie ⍝ find first NL
    size←⎕NSIZE tie ⍝ total file length
    starts←lengthׯ1+⍺?size÷length ⍝ beginning of each line
    ↑⎕NREAD¨tie 82length∘,¨starts ⍝ read each line as character and convert list to table
}

* Mogę zapisać bajt uruchamiając w trybie 0 pochodzenia, co jest standardem w niektórych systemach APL: Wyjmij ¯1+i włóż 1+wcześniej 10.


Ahh .. APL :) Czy jest jakiś sposób na przetestowanie tego kodu w systemie Linux?

@Lembik Oczywiście, ten kod jest wieloplatformowy. Pobierz z dyalog.com
— Adám

Ponieważ nie czytam APL, czy możesz wyjaśnić kod? Trudne części to próbkowanie linii bez zamiany i przeskakiwanie bezpośrednio do właściwego miejsca w pliku, aby odczytać linie.

@Lembik Ta część jest łatwa. Argument Argumentem NREAD jest TieNumber ConversionCode BytesToRead [StartByte]. Czyta tylko wymagane bajty. Reszta zastanawia się, co przeczytać.
— Adám

@Lembik Jestem ciekawy, dlaczego moja odpowiedź nie wygrała nagrody.
— Adám

7

Rubin, 104 94 92 90 bajtów

Nazwa pliku i liczba wierszy są przekazywane do wiersza poleceń. Na przykład, jeśli program ma shuffle.rbnazwę pliku a.txt, uruchomruby shuffle.rb a.txt 3 trzy losowe linie.

-4 bajty od odkrycia openskładni w Rubim zamiastFile.new

f=open$*[0]
puts [*0..f.size/n=f.gets.size+1].sample($*[1].to_i).map{|e|f.seek n*e;f.gets}

Oto 85-bajtowe rozwiązanie funkcji anonimowej, które jako argument przyjmuje ciąg i liczbę.

->f,l{f=open f;puts [*0..f.size/n=f.gets.size+1].sample(l).map{|e|f.seek n*e;f.gets}}

Poniżej 100 bajtów! Być może Ruby jest najlepszym językiem golfowym. Czy „próbka” pozwala uniknąć powtórzeń?

@Lembik ruby-doc.org/core-2.2.0/Array.html#method-i-sample Pozwala to uniknąć powtórzeń. Nie mów mi ... czy miałam mieć powtórzenia?
— Wartość tuszu

Nie, jesteś idealny :)

Czy możesz zapisać jakieś bajty, czytając ze standardowego wejścia? ruby shuffle.rb 3 < a.txtdaje widoczne stdin. Jednak IDK Ruby.
— Peter Cordes

1
@PeterCordes To ma sens, ale jak wspomniano, punktem niepowodzenia jest to, że Ruby nie jest w stanie odczytać rozmiaru pliku standardowego, więc to nie zadziałało.
— Wartość tuszu

5

Haskell, 240 224 236 bajtów

import Test.QuickCheck
import System.IO
g=hGetLine
main=do;f<-getLine;n<-readLn;h<-openFile f ReadMode;l<-(\x->1+sum[1|_<-x])<$>g h;s<-hFileSize h;generate(shuffle[0..div s l-1])>>=mapM(\p->hSeek h(toEnum 0)(l*p)>>g h>>=putStrLn).take n

Odczytuje nazwę pliku in ze standardowego wejścia.

Jak to działa:

main=do
  f<-getLine                   -- read file name from stdin
  n<-readLn                    -- read n from stdin
  h<-openFile f ReadMode       -- open the file
  l<-(\x->1+sum[1|_<-x])<$>g h -- read first line and bind l to it's length +1
                               -- sum[1|_<-x] is a custom length function
                               -- because of type restrictions, otherwise I'd have
                               -- to use "toInteger.length"
  s<-hFileSize h               -- get file size
  generate(shuffle[0..div s l-1])>>=
                               -- shuffle all possible line numbers 
  mapM (\->p  ...  ).take n    -- for each of the first n shuffled line numbers 
     hSeek h(toEnum 0).(l*p)>> -- jump to that line ("toEnum 0" is short for "AbsoluteSeek")
     g h>>=                    -- read a line from current position
     putStrLn                  -- and print

Uruchomienie tego programu w przypadku plików z wieloma wierszami zajmuje dużo czasu i pamięci z powodu okropnej nieefektywności shuffle funkcji.

Edycja: Brakowało mi części „losowo bez wymiany” (dziękuję @feersum za zauważenie!).


Haskell kołysze :)

1
Jak uniknąć wybrania linii, która została już wybrana?
— feersum

@feersum: och, tęskniłem za tą częścią. Naprawiony.
— nimi

Widzę, że stackoverflow.com/questions/13779630/… jest nieco gadatliwy!

1
Może powinno być osobne wyzwanie przy próbkowaniu bez wymiany na małej przestrzeni.

3

PowerShell v2 +, 209 bajtów

param($a,$n)
$f=New-Object System.IO.FileStream $a,"Open"
for(;$f.ReadByte()-ne10){$l++}
$t=$f.Length/++$l-1
[byte[]]$z=,0*$l
0..$t|Get-Random -c $n|%{$a=$f.Seek($l*$_,0);$a=$f.Read($z,0,$l-1);-join[char[]]$z}

Pobiera dane wejściowe $ajako nazwę pliku i $nliczbę wierszy. Zauważ, że$a musi to być pełna ścieżka pliku i zakłada się, że jest to kodowanie ANSI.

Następnie konstruujemy nowy FileStreamobiekt i mówimy mu, aby uzyskał dostęp do pliku za $apomocąOpen uprawnieniami.

forPętla .Read()s przez pierwszą linię, aż trafiliśmy na \ncharakter, zwiększając nasz licznik linia długości każdego znaku. Następnie ustawiamy $trówny rozmiar pliku (tj. Długość strumienia) podzielony przez liczbę znaków w wierszu (plus jeden, więc zlicza terminator), minus jeden (ponieważ mamy indeksowane zero). Następnie budujemy nasz bufor$z aby był również długością linii.

Ostatnia linia konstruuje tablicę dynamiczną z ..operatorem zakresu. 1 rura takim, że matryca do Get-Randomz -Count z $nlosowo wybierać $nnumery linii bez powtarzania. Szczęśliwe liczby są połączone w pętlę z |%{...}. Każdą iterację przechodzimy .Seekdo określonej lokalizacji, a następnie .Readw postaci znaków zapisanych w linii $z. Przerzucamy ponownie $zjako tablicę -joinznaków i razem, pozostawiając wynikowy ciąg w potoku, a dane wyjściowe są niejawne na końcu programu.

Technicznie powinniśmy zakończyć $f.Close()wezwaniem do zamknięcia pliku, ale to kosztuje bajty! : p

Przykład

a.txt:
a0000000000000000000000000000000000000000000000001
a0000000000000000000000000000000000000000000000002
a0000000000000000000000000000000000000000000000003
a0000000000000000000000000000000000000000000000004
a0000000000000000000000000000000000000000000000005
a0000000000000000000000000000000000000000000000006
a0000000000000000000000000000000000000000000000007
a0000000000000000000000000000000000000000000000008
a0000000000000000000000000000000000000000000000009
a0000000000000000000000000000000000000000000000010

PS C:\Tools\Scripts\golfing> .\read-n-random-lines.ps1 "c:\tools\scripts\golfing\a.txt" 5
a0000000000000000000000000000000000000000000000002 
a0000000000000000000000000000000000000000000000001 
a0000000000000000000000000000000000000000000000004 
a0000000000000000000000000000000000000000000000010 
a0000000000000000000000000000000000000000000000006 

1 Technicznie oznacza to, że możemy obsłużyć maksymalnie 50 000 linii, ponieważ jest to największy zakres, który można dynamicznie utworzyć w ten sposób. : - / Ale nie możemy po prostu zapętlić czasów Get-Randompoleceń $n, odrzucając duplikaty każdej pętli, ponieważ nie jest to deterministyczne ...


2

Python 3, 146 139 bajtów

from random import*
i=input
f=open(i())
l=len(f.readline())
[(f.seek(v*l),print(f.read(l)))for v in sample(range(f.seek(0,2)//l),int(i()))]
#print is here^

Dane wejściowe: [nazwa pliku] \ n [wiersze] \ n

To rozwiązanie mocno ukradło z @pppery, ale jest tylko python3.5 i jest kompletnym programem.

Edycja: Dzięki @Mego za zakres wbudowany i kompatybilność z python3.x. Edycja2: Wyjaśnienie, gdzie jest wydruk, ponieważ otrzymałem dwa komentarze na jego temat. (Komentarz oczywiście nie jest częścią kodu ani liczby bajtów).


Dziękuję Ci! Która część to tylko Python 3.5?

2
r=range(f.seek(0,2)//l)zadziała, co zrzuca 3 bajty i eliminuje potrzebę 3.5. Co więcej, zmniejsz liczbę kolejnych 3 bajtów, umieszczając rangepołączenie w samplepołączeniu. Ponadto nie jest to kompletny program - musisz wydrukować listę.
— Mego

@Lembik: To było tylko 3.5, ponieważ użyłem, r=[*range(f.seek(0,2)//l)]ponieważ myślałem, że nie mogę samplegeneratora. Okazuje się, że mogłem. @Mega: Jest kompletny, ponieważ wypisuje wszystkie wiersze wewnątrz listyprint(f.read(l))
— Alexander Nigl

Potrzebujesz jednak wyciągu drukowanego.

print znajduje się wewnątrz listy.
— Alexander Nigl

2

Lua, 126 122

r=io.read;f=io.open(r())c=2+f:read():len()for i=1,r()do f:seek("set",c*math.random(0,f:seek("end")/c-1))print(f:read())end

Wykorzystuje 2 bajty do podziału linii. Zmień 2 na 1 dla 1. Mam go tylko jako 2, ponieważ taki był mój plik testowy.

Znalazłem się pod wpisem PHP, ale nadal 2 miejsce (obecnie). Przeklnij, Ruby, wejdź!


1
Lua był pierwszym językiem programowania, którego się nauczyłem, a mimo tego wszystkiego, czego się od tego czasu nauczyłem, nadal jest moim ulubionym. Jest tak wszechstronny ze względu na łatwość pisania.
— Blab

2

Bash (cóż, coreutils), 100 bajtów

n=`head -1 $2|wc -c`;shuf -i0-$[`stat -c%s $2`/$n] -n$1|xargs -i dd if=$2 bs=$n skip={} count=1 2>&-

Wyjaśnienie

Pozwala to uniknąć odczytu całego pliku przy użyciu dd do wyodrębnienia potrzebnych fragmentów pliku bez odczytu pliku, niestety kończy się dość duży ze wszystkimi opcjami, które musimy określić:

ifto plik wejściowy
bsto rozmiar bloku (tutaj ustawiamy, do $nktórego jest długość pierwszego wiersza
skipustawiana na losowe liczby całkowite wyodrębnione z shufi jest równa ibswartości pomijającej skip* ibsbajtów
countliczbę ibsodcinków długości do zwrócenia
status=none jest potrzebna do usunięcia informacje zwykle wysyłane przezdd

Otrzymujemy długość linii za pomocą head -1 $2|wc -ci rozmiar pliku za pomocą stat -c%s $2.

Stosowanie

Zapisz powyżej jako file.shi uruchom przy użyciu file.sh n filename.

Czasy

time ~/randlines.sh 4 test.txt
9412647
4124435
7401105
1132619

real    0m0.125s
user    0m0.035s
sys     0m0.061s

vs.

time shuf -n4 test.txt
1204350
3496441
3472713
3985479

real    0m1.280s
user    0m0.287s
sys     0m0.272s

Razy powyżej dla pliku 68 MB wygenerowanego przy użyciu seq 1000000 9999999 > test.txt.

Dzięki @PeterCordes za jego wskazówkę -1!


1
Zawsze lubię szybką odpowiedź, ale czy możesz wyjaśnić, w jaki sposób nie odczytuje ona całego pliku?

2
@Lembik dodał wyjaśnienie!
— Dom Hastings

1
Możesz bs=zamiast tego ibs=, ponieważ ustawienie obsrównież jest w porządku. Chyba nie można zastąpić if=$2z <$2chociaż, ponieważ jest to nadal xargsjest linia poleceń. \<$2też nie działa (xargs używa exec bezpośrednio, bez powłoki).
— Peter Cordes

Mam nadzieję, że nie jest to zbyt wiele, ale uwielbiam tę odpowiedź :) Właśnie przetestowałem ją z plikiem 1 GB.

1
re: przekierowanie stderr do stdin: Możesz również zamknąć stderr 2>&-, więc nie ma niebezpieczeństwa, że ​​dane wyjściowe będą nigdzie (np. jeśli stdin będzie deskryptorem pliku do odczytu i zapisu). Działa z GNU dd: Nadal produkuje swój stdoutprzed próbą i bez zapisu do stderr.
— Peter Cordes

1

Python 3 - 161 160 149 bajtów

from random import*;
def f(n,g):f=open(g);l=len(f.readline());r=list(range(f.seek(0,2)/l));shuffle(r);[(f.seek(v*l),print(f.read(l)))for v in r[:k]]

Ten kod definiuje funkcję, która jest wywoływana jak f(10,'input.txt')


1
Wyzwanie wymaga pełnego programu, więc obawiam się, że definicja funkcji nie wystarczy.
— nimi

Aby zaoszczędzić bajt, usuń spację między importem a *.
— mriklojn

1
@nimi Wymaganie pełnego programu do tego wyzwania wydaje się arbitralnie zastępować domyślne reguły formatu kodu
— pppery

@ppperry: tak, może, ale tak właśnie jest.
— nimi

Aby uzyskać długość pliku, możesz f.seek (0,2) , co powoduje, że importowane os i os.stat stają się przestarzałe.
— Alexander Nigl

1

C # 259 bajtów bez duplikatów

class Program{static void Main(string[]a){int c=Convert.ToInt32(a[1]);var h=File.ReadLines(a[0]);HashSet<int>n=new HashSet<int>();while(n.Count<c)n.Add(new Random().Next(0,h.Count()));for(;c>0;c--)Console.WriteLine(h.Skip(n.ElementAt(c-1)).Take(1).First());}}

Nie golfił

class Program{static void Main(string[] a)
{
        int c = Convert.ToInt32(a[1]);
        var h = File.ReadLines(a[0]);
        HashSet<int> n = new HashSet<int>();
        while (n.Count < c)
            n.Add(new Random().Next(0, h.Count()));           
        for (; c > 0; c--)
            Console.WriteLine(h.Skip(n.ElementAt(c-1)).Take(1).First());
    }
}

File.ReadLines jest leniwy. Ma to dodatkową zaletę, że wszystkie linie mogą mieć różną długość.

Uruchomienie byłoby:

sample.exe a.txt 10000

C # 206 bajtów z duplikatami

class Program{static void Main(string[]a){var n=new Random();int c=Convert.ToInt32(a[1]);var h=File.ReadLines(a[0]);for(;c>0;c--)Console.WriteLine(h.Skip((int)(n.NextDouble()*h.Count())).Take(1).First());}}

Nie golfił

class Program
{
    static void Main(string[] a)
    {
        Random n = new Random();
        int c = Convert.ToInt32(a[1]);
        var h = File.ReadLines(a[0]);
        for (; c > 0; c--)
            Console.WriteLine(h.Skip((int)(n.NextDouble()*h.Count())).Take(1).First());
    }
}

Nie w pełni podążam za twoim rozwiązaniem. Jeśli wszystkie linie mają różne długości, zadanie jest niemożliwe. A także, jak losowo próbujesz linie bez zamiany dokładnie? Przepraszam, że mój C # nie jest wystarczająco dobry.

@Lembik Masz rację, nie myślałem o duplikatach. I mogę policzyć liczbę linii i wyodrębnić linie według numeru bielizny, dlatego linie mogą mieć zmienną długość.
— Master117,

Ale musisz przeskoczyć do lokalizacji w pliku tylko wiedząc, że numer linii, prawda? Nie możesz powiedzieć, gdzie to jest, chyba że wszystkie linie mają tę samą długość.

@Lembik File.ReadLines („pathToFile”) tworzy leniwe wyliczenie na wszystkich liniach pliku, File.ReadLines („pathToFile”). ElementAt (19) zwraca 19. linię pliku. Trochę jak Mapa wszystkich Linestarts.
— Master117,

Nie sądzę, aby Lazy wyliczenie skakało (lub szuka) w pliku ze smutkiem. Więc to nie pasuje obecnie do reguł.

1

Python (141 bajtów)

Utrzymuje każdą linię z jednakowym prawdopodobieństwem, użyj również z rurami. Nie odpowiada to jednak ograniczeniu pytania z wyprzedzeniem ...

Zastosowanie cat largefile | python randxlines.py 100lub python randxlines 100 < largefile(jak wskazano @petercordes)

import random,sys
N=int(sys.argv[1])
x=['']*N
for c,L in enumerate(sys.stdin):
    t=random.randrange(c+1)
    if(t<N):x[t] = L
print("".join(x))

3
Chodzi o to, że musisz szukać w strumieniu wejściowym. Prawdopodobnie powinieneś powiedzieć, że jest to część ograniczeń pytania, które ignorujesz (chociaż użycie przykładowego odczytu z potoku wyjaśnia to dość wyraźnie). Czytanie ze standardowego python ./randxlines.py 100 < largefileprzydałoby się jednak dla poprawnej odpowiedzi: w takim przypadku stdinbędzie widoczne.
— Peter Cordes
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.