Bagaimana saya bisa mengurutkan satu baris item yang dibatasi secara numerik?


11

Saya memiliki garis (atau banyak garis) angka yang dibatasi oleh karakter arbitrer. Alat UNIX apa yang dapat saya gunakan untuk mengurutkan item setiap baris secara numerik, mempertahankan pembatas?

Contohnya termasuk:

  • daftar angka; masukan 10 50 23 42:; diurutkan:10 23 42 50
  • Alamat IP; masukan 10.1.200.42:; diurutkan:1.10.42.200
  • CSV; masukan 1,100,330,42:; diurutkan:1,42,100,330
  • dibatasi pipa; masukan 400|500|404:; diurutkan:400|404|500

Karena pembatas adalah arbitrer, jangan ragu untuk memberikan (atau memperluas) jawaban menggunakan pembatas satu karakter yang Anda pilih.


8
Anda harus mempostingnya di codegolf :)
ivanivan

1
ada pertanyaan serupa juga di sini saya ingin menambahkan tautannya Alfabetisasi kata-kata dalam nama file menggunakan sort?
αғsнιη

Hanya sebuah petunjuk yang cutmendukung pembatas sewenang-wenang dengan -dopsinya.
Oleg Lobachev

Tolong jelaskan apakah keempat contoh DSV itu ada di file yang sama, atau sampel dari empat file berbeda.
agc

2
Melihat beberapa komentar lain: pembatas adalah arbitrer, tetapi akan digunakan secara konsisten dalam input. Asumsikan intelijen pada bagian dari produsen data sehingga mereka tidak akan menggunakan koma sebagai pembatas dan dalam data (misalnya, 4,325 comma 55 comma 42,430tidak akan terjadi, atau 1.5 period 4.2).
Jeff Schaller

Jawaban:


12

Anda dapat mencapai ini dengan:

tr '.' '\n' <<<"$aline" | sort -n | paste -sd'.' -

ganti titik . dengan pembatas Anda.
tambahkan -uke sortperintah di atas untuk menghapus duplikat.


atau dengan gawk( GNU awk ) kami dapat memproses banyak baris sementara di atas juga dapat diperpanjang:

gawk -v SEP='*' '{ i=0; split($0, arr, SEP); 
    while ( ++i<=asort(arr) ){ printf("%s%s", i>1?SEP:"", arr[i]) }; 
        print "" 
}' infile

ganti *sebagai pemisah bidang SEP='*'dengan pembatas Anda .


Catatan:
Anda mungkin perlu menggunakan -g, --general-numeric-sortopsi sortalih - alih -n, --numeric-sortmenangani kelas angka apa pun (bilangan bulat, float, ilmiah, heksadesimal, dll).

$ aline='2e-18,6.01e-17,1.4,-4,0xB000,0xB001,23,-3.e+11'
$ tr ',' '\n' <<<"$aline" |sort -g | paste -sd',' -
-3.e+11,-4,2e-18,6.01e-17,1.4,23,0xB000,0xB001

Dalam awktidak ada perubahan kebutuhan, masih akan menangani mereka.


10

Menggunakan perlada versi yang jelas; pisahkan data, sortir, gabungkan kembali.

Pembatas perlu didaftar dua kali (sekali dalam splitdan sekali dalam join)

misalnya untuk a ,

perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'

Begitu

echo 1,100,330,42 | perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'
1,42,100,330

Karena itu splitadalah regex, karakter mungkin perlu mengutip:

echo 10.1.200.42 | perl -lpi -e '$_=join(".",sort {$a <=> $b} split(/\./))'
1.10.42.200

Dengan menggunakan opsi -adan -F, dimungkinkan untuk menghapus pemisahan. Dengan -ploop, seperti sebelumnya dan atur hasilnya $_, yang secara otomatis akan mencetak:

perl -F'/\./' -aple '$_=join(".", sort {$a <=> $b} @F)'

4
Anda dapat menggunakan -lopsi alih-alih menggunakan chomp. Itu juga menambah kembali baris baru saat dicetak. Lihat juga -a(dengan -F) untuk bagian pemisahan.
Stéphane Chazelas

1
Dengan -ldan -F, itu bahkan lebih baik:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
muru

@ StéphaneChazelas terima kasih atas -lpilihannya; Aku merindukan itu!
Stephen Harris

1
@muru Saya awalnya tidak menggunakan -Fflag karena tidak bekerja dengan baik di semua versi (misalnya baris Anda di CentOS 7 - perl 5.16.3 - mengembalikan output kosong, meskipun berfungsi dengan baik pada Debian 9). Tetapi dikombinasikan dengan -pitu memberikan hasil yang sedikit lebih kecil, jadi saya menambahkan itu sebagai alternatif untuk jawabannya. menunjukkan bagaimana -Fbisa digunakan. Terima kasih!
Stephen Harris

2
@StephenHarris itu karena versi yang lebih baru dari perl otomatis menambah -adan -npilihan ketika -Fdigunakan dan -nketika -adigunakan ... jadi hanya mengubah -leke-lane
Sundeep

4

Menggunakan Python dan ide serupa seperti pada jawaban Stephen Harris :

python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' <delmiter>

Jadi sesuatu seperti:

$ cat foo
10.129.3.4
1.1.1.1
4.3.2.1
$ python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' . < foo
3.4.10.129
1.1.1.1
1.2.3.4

Sayangnya harus melakukan I / O secara manual membuat ini jauh lebih elegan daripada versi Perl.



3

Kulit

Memuat bahasa tingkat yang lebih tinggi membutuhkan waktu.
Untuk beberapa baris, shell itu sendiri bisa menjadi solusi.
Kita dapat menggunakan perintah eksternal sort, dan dari perintah tr. Satu cukup efisien dalam menyortir garis dan yang lainnya efektif untuk mengubah satu pembatas menjadi baris baru:

#!/bin/bash
shsort(){
           while IFS='' read -r line; do
               echo "$line" | tr "$1" '\n' |
               sort -n   | paste -sd "$1" -
           done <<<"$2"
    }

shsort ' '    '10 50 23 42'
shsort '.'    '10.1.200.42'
shsort ','    '1,100,330,42'
shsort '|'    '400|500|404'
shsort ','    '3 b,2       x,45    f,*,8jk'
shsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

Ini perlu bash karena penggunaannya <<<saja. Jika itu diganti dengan di sini-doc, solusinya berlaku untuk posix.
Hal ini dapat mengurutkan bidang dengan tab, spasi atau karakter shell gumpal ( *, ?, [). Bukan baris baru karena setiap baris sedang diurutkan.

Ubah <<<"$2"untuk <"$2"memproses nama file dan menyebutnya seperti:

shsort '.'    infile

Pembatas adalah sama untuk seluruh file. Jika itu adalah batasan, itu bisa diperbaiki.

Namun file dengan hanya 6000 baris membutuhkan waktu 15 detik untuk diproses. Sungguh, shell bukanlah alat terbaik untuk memproses file.

Awk

Untuk lebih dari beberapa baris (lebih dari beberapa 10-an) lebih baik menggunakan bahasa pemrograman nyata. Solusi awk bisa berupa:

#!/bin/bash
awksort(){
           gawk -v del="$1" '{
               split($0, fields, del)
               l=asort(fields)
               for(i=1;i<=l;i++){
                   printf( "%s%s" , (i==0)?"":del , fields[i] )
               }
               printf "\n"
           }' <"$2"
         }

awksort '.'    infile

Yang hanya membutuhkan 0,2 detik untuk file 6000 baris yang sama yang disebutkan di atas.

Memahami bahwa <"$2"file untuk dapat diubah kembali ke <<<"$2"untuk baris di dalam variabel shell.

Perl

Solusi tercepat adalah perl.

#!/bin/bash
perlsort(){  perl -lp -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' <<<"$2";   }

perlsort ' '    '10 50 23 42'
perlsort '.'    '10.1.200.42'
perlsort ','    '1,100,330,42'
perlsort '|'    '400|500|404'
perlsort ','    '3 b,2       x,45    f,*,8jk'
perlsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

Jika Anda ingin mengurutkan perubahan file secara <<<"$a"sederhana "$a"dan menambahkan -ike opsi perl untuk membuat edisi file "di tempat":

#!/bin/bash
perlsort(){  perl -lpi -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' "$2"; }

perlsort '.' infile; exit

2

Menggunakan seduntuk mengurutkan oktet dari alamat IP

sedtidak memiliki sortfungsi bawaan, tetapi jika data Anda cukup dibatasi dalam jangkauan (seperti dengan alamat IP), Anda dapat membuat skrip sed yang secara manual mengimplementasikan semacam gelembung sederhana . Mekanisme dasarnya adalah untuk mencari nomor yang berdekatan yang rusak. Jika nomornya tidak sesuai pesanan, tukar.

The sedScript itu sendiri berisi dua perintah pencarian dan-swap untuk setiap pasangan out-of-order nomor: satu untuk dua pasang pertama oktet (memaksa pembatas tertinggal untuk hadir untuk menandai akhir dari oktet ketiga), dan kedua untuk pasangan ketiga oktet (diakhiri dengan EOL). Jika swap terjadi, program bercabang ke bagian atas skrip, mencari angka-angka yang rusak. Kalau tidak, ia keluar.

Script yang dihasilkan adalah, sebagian:

$ head -n 3 generated.sed
:top
s/255\.254\./254.255./g; s/255\.254$/254.255/
s/255\.253\./253.255./g; s/255\.253$/253.255/

# ... middle of the script omitted ...

$ tail -n 4 generated.sed
s/2\.1\./1.2./g; s/2\.1$/1.2/
s/2\.0\./0.2./g; s/2\.0$/0.2/
s/1\.0\./0.1./g; s/1\.0$/0.1/
ttop

Pendekatan ini mengkodekan periode sebagai pembatas, yang harus diloloskan, karena jika tidak maka akan menjadi "spesial" untuk sintaks ekspresi reguler (memungkinkan karakter apa pun).

Untuk menghasilkan skrip sed, loop ini akan melakukan:

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; n-- )); do
  for (( m = n - 1; m >= 0; m-- )); do
    printf '%s; %s\n' "s/$n\\.$m\\./$m.$n./g" "s/$n\\.$m\$/$m.$n/"
  done
done

echo 'ttop'

Redirect output skrip itu ke file lain, misalnya sort-ips.sed.

Kemudian contoh dijalankan dapat terlihat seperti:

ip=$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256))
printf '%s\n' "$ip" | sed -f sort-ips.sed

Variasi berikut pada skrip pembuat menggunakan penanda kata batas \<dan \>untuk menghilangkan kebutuhan substitusi kedua. Ini juga mengurangi ukuran skrip yang dihasilkan dari 1,3 MB menjadi hanya di bawah 900 KB bersama dengan sangat mengurangi waktu menjalankan sedsendiri (menjadi sekitar 50% -75% dari aslinya, tergantung pada apa sedimplementasi yang digunakan):

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; --n )); do
  for (( m = n - 1; m >= 0; --m )); do
      printf '%s\n' "s/\\<$n\\>\\.\\<$m\\>/$m.$n/g"
  done
done

echo 'ttop'

1
Gagasan yang menarik, tetapi hal itu agaknya terlalu menyulitkan.
Matt

1
@ Matt Ini intinya. Menyortir sesuatu dengan sedkonyol, itulah sebabnya ini merupakan tantangan yang menarik.
Kusalananda

2

Di sini beberapa bash yang menebak pembatas dengan sendirinya:

#!/bin/bash

delimiter="${1//[[:digit:]]/}"
if echo $delimiter | grep -q "^\(.\)\1\+$"
then
  delimiter="${delimiter:0:1}"
  if [[ -z $(echo $1 | grep "^\([0-9]\+"$delimiter"\([0-9]\+\)*\)\+$") ]]
  then
    echo "You seem to have empty fields between the delimiters."
    exit 1
  fi
  if [[ './\' == *$delimiter* ]]
  then
    n=$( echo $1 | sed "s/\\"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/\\"$delimiter"/g")
  else
    n=$( echo $1 | sed "s/"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/"$delimiter"/g")
  fi
  echo ${n%$delimiter}
  exit 0
else
  echo "The string does not consist of digits separated by one unique delimiter."
  exit 1
fi

Mungkin tidak terlalu efisien atau bersih tetapi berfungsi.

Gunakan seperti bash my_script.sh "00/00/18/29838/2".

Mengembalikan kesalahan ketika pembatas yang sama tidak digunakan secara konsisten atau ketika dua atau lebih pembatas saling mengikuti.

Jika pembatas yang digunakan adalah karakter khusus, maka ia akan keluar (jika tidak sedmengembalikan kesalahan).


Itu mengilhami ini .
agc

2

Jawaban ini didasarkan pada kesalahpahaman tentang Q., tetapi dalam beberapa kasus itu tetap benar. Jika input seluruhnya bilangan alami , dan hanya memiliki satu pembatas per-baris, (seperti dengan sampel data dalam Q.), ia bekerja dengan benar. Ini juga akan menangani file dengan garis yang masing-masing memiliki pembatas sendiri, yang sedikit lebih dari apa yang diminta.

Shell ini berfungsi readdari input standar, menggunakan substitusi parameter POSIX untuk menemukan pembatas khusus pada setiap baris, (disimpan dalam $d), dan digunakan truntuk mengganti $ddengan baris baru \ndan sortdata baris itu, kemudian mengembalikan pembatas asli setiap baris:

sdn() { while read x; do
            d="${x#${x%%[^0-9]*}}"   d="${d%%[0-9]*}"
            x=$(echo -n "$x" | tr "$d" '\n' | sort -g | tr '\n' "$d")
            echo ${x%?}
        done ; }

Diterapkan pada data yang diberikan dalam OP :

printf "%s\n" "10 50 23 42" "10.1.200.42" "1,100,330,42" "400|500|404" | sdn

Keluaran:

10 23 42 50
1.10.42.200
1,42,100,330
400|404|500

Pembatas dalam garis apa pun akan konsisten; solusi umum yang memungkinkan pengguna untuk menyatakan pembatas itu hebat, tetapi jawaban dapat mengasumsikan pembatas apa pun yang masuk akal bagi mereka (karakter tunggal dan tidak ada dalam data numerik itu sendiri).
Jeff Schaller

2

Untuk pembatas yang berubah-ubah:

perl -lne '
  @list = /\D+|\d+/g;
  @sorted = sort {$a <=> $b} grep /\d/, @list;
  for (@list) {$_ = shift@sorted if /\d/};
  print @list'

Pada input seperti:

5,4,2,3
6|5,2|4
There are 10 numbers in those 3 lines

Memberikan:

2,3,4,5
2|4,5|6
There are 3 numbers in those 10 lines

0

Ini harus menangani pembatas non-digit (0-9). Contoh:

x='1!4!3!5!2'; delim=$(echo "$x" | tr -d 0-9 | cut -b1); echo "$x" | tr "$delim" '\n' | sort -g | tr '\n' "$delim" | sed "s/$delim$/\n/"

Keluaran:

1!2!3!4!5

0

Dengan perl:

$ # -a to auto-split on whitespace, results in @F array
$ echo 'foo baz v22 aimed' | perl -lane 'print join " ", sort @F'
aimed baz foo v22
$ # {$a <=> $b} for numeric comparison, {$b <=> $a} will give descending order
$ echo '1,100,330,42' | perl -F, -lane 'print join ",", sort {$a <=> $b} @F'
1,42,100,330

Dengan ruby, yang agak mirip denganperl

$ # -a to auto-split on whitespace, results in $F array
$ # $F is sorted and then joined using the given string
$ echo 'foo baz v22 aimed' | ruby -lane 'print $F.sort * " "'
aimed baz foo v22

$ # (&:to_i) to convert string to integer
$ echo '1,100,330,42' | ruby -F, -lane 'print $F.sort_by(&:to_i) * ","'
1,42,100,330

$ echo '10.1.200.42' | ruby -F'\.' -lane 'print $F.sort_by(&:to_i) * "."'
1.10.42.200


Perintah kustom dan hanya melewati string pembatas (bukan regex). Akan berfungsi jika input memiliki data mengambang juga

$ # by default join uses value of $,
$ sort_line(){ ruby -lne '$,=ENV["d"]; print $_.split($,).sort_by(&:to_f).join' ; }

$ s='103,14.5,30,24'
$ echo "$s" | d=',' sort_line
14.5,24,30,103
$ s='10.1.200.42'
$ echo "$s" | d='.' sort_line
1.10.42.200

$ # for file input
$ echo '123--87--23' > ip.txt
$ echo '3--12--435--8' >> ip.txt
$ d='--' sort_line <ip.txt
23--87--123
3--8--12--435


Perintah khusus untuk perl

$ sort_line(){ perl -lne '$d=$ENV{d}; print join $d, sort {$a <=> $b} split /\Q$d/' ; }
$ s='123^[]$87^[]$23'
$ echo "$s" | d='^[]$' sort_line 
23^[]$87^[]$123


Bacaan lebih lanjut - Saya sudah memiliki daftar ini perl / ruby ​​one-liners


0

Berikut ini adalah variasi pada jawaban Jeff dalam arti bahwa ia menghasilkan sedskrip yang akan melakukan semacam Bubble, tetapi cukup berbeda untuk menjamin jawabannya sendiri.

Perbedaannya adalah bahwa alih-alih menghasilkan O (n ^ 2) ekspresi reguler dasar, ini menghasilkan O (n) perluasan ekspresi reguler. Script yang dihasilkan akan sekitar 15 KB besar. Waktu menjalankan sedskrip dalam sepersekian detik (dibutuhkan waktu lebih lama untuk menghasilkan skrip).

Ini dibatasi untuk mengurutkan bilangan bulat positif yang dibatasi oleh titik-titik, tetapi tidak terbatas pada ukuran bilangan bulat (hanya meningkatkan 255loop utama), atau jumlah bilangan bulat. Pembatas dapat diubah dengan mengubah delim='.'kode.

Ini dilakukan kepala saya untuk mendapatkan ekspresi reguler yang benar, jadi saya akan pergi menjelaskan detail untuk hari lain.

#!/bin/bash

# This function creates a extended regular expression
# that matches a positive number less than the given parameter.
lt_pattern() {
    local n="$1"  # Our number.
    local -a res  # Our result, an array of regular expressions that we
                  # later join into a string.

    for (( i = 1; i < ${#n}; ++i )); do
        d=$(( ${n: -i:1} - 1 )) # The i:th digit of the number, from right to left, minus one.

        if (( d >= 0 )); then
            res+=( "$( printf '%d[0-%d][0-9]{%d}' "${n:0:-i}" "$d" "$(( i - 1 ))" )" )
        fi
    done

    d=${n:0:1} # The first digit of the number.
    if (( d > 1 )); then
        res+=( "$( printf '[1-%d][0-9]{%d}' "$(( d - 1 ))" "$(( ${#n} - 1 ))" )" )
    fi

    if (( n > 9 )); then
        # The number is 10 or larger.
        res+=( "$( printf '[0-9]{1,%d}' "$(( ${#n} - 1 ))" )" )
    fi

    if (( n == 1 )); then
        # The number is 1. The only thing smaller is zero.
        res+=( 0 )
    fi

    # Join our res array of expressions into a '|'-delimited string.
    ( IFS='|'; printf '%s\n' "${res[*]}" )
}

echo ':top'

delim='.'

for (( n = 255; n > 0; --n )); do
    printf 's/\\<%d\\>\\%s\\<(%s)\\>/\\1%s%d/g\n' \
        "$n" "$delim" "$( lt_pattern "$n" )" "$delim" "$n"
done

echo 'ttop'

Script akan terlihat seperti ini:

$ bash generator.sh >script.sed
$ head -n 5 script.sed
:top
s/\<255\>\.\<(25[0-4][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.255/g
s/\<254\>\.\<(25[0-3][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.254/g
s/\<253\>\.\<(25[0-2][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.253/g
s/\<252\>\.\<(25[0-1][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.252/g
$ tail -n 5 script.sed
s/\<4\>\.\<([1-3][0-9]{0})\>/\1.4/g
s/\<3\>\.\<([1-2][0-9]{0})\>/\1.3/g
s/\<2\>\.\<([1-1][0-9]{0})\>/\1.2/g
s/\<1\>\.\<(0)\>/\1.1/g
ttop

Gagasan di balik ekspresi reguler yang dihasilkan adalah untuk mencocokkan pola untuk angka yang kurang dari setiap bilangan bulat; dua nomor itu akan rusak, dan begitu juga ditukar. Ekspresi reguler dikelompokkan ke dalam beberapa opsi ATAU. Perhatikan kisaran yang ditambahkan ke setiap item, kadang-kadang {0}, yang berarti item segera-sebelumnya harus dihilangkan dari pencarian. Opsi regex, dari kiri ke kanan, mencocokkan nomor yang lebih kecil dari angka yang diberikan oleh:

  • tempat yang
  • tempat puluhan
  • ratusan tempat
  • (diteruskan sesuai kebutuhan, untuk jumlah yang lebih besar)
  • atau dengan menjadi lebih kecil dalam besarnya (jumlah digit)

Untuk menguraikan contoh, ambil 101(dengan ruang tambahan untuk dibaca):

s/ \<101\> \. \<(10[0-0][0-9]{0} | [0-9]{1,2})\> / \1.101 /g

Di sini, pergantian pertama memungkinkan angka 100 hingga 100; pergantian kedua memungkinkan 0 hingga 99.

Contoh lain adalah 154:

s/ \<154\> \. \<(15[0-3][0-9]{0} | 1[0-4][0-9]{1} | [0-9]{1,2})\> / \1.154 /g

Di sini opsi pertama memungkinkan 150 hingga 153; yang kedua memungkinkan 100 hingga 149, dan yang terakhir memungkinkan 0 hingga 99.

Menguji empat kali dalam satu lingkaran:

for test_run in {1..4}; do
    nums=$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 ))
    printf 'nums=%s\n' "$nums"
    sed -E -f script.sed <<<"$nums"
done

Keluaran:

nums=90.19.146.232
19.90.146.232
nums=8.226.70.154
8.70.154.226
nums=1.64.96.143
1.64.96.143
nums=67.6.203.56
6.56.67.203

-2

Membagi input menjadi beberapa baris

Dengan menggunakan tr, Anda dapat membagi input menggunakan pembatas acak menjadi beberapa baris.

Input ini kemudian dapat dijalankan sort(menggunakan -njika inputnya numerik).

Jika Anda ingin mempertahankan pembatas di output, Anda dapat menggunakan trlagi untuk menambahkan kembali pembatas.

mis menggunakan ruang sebagai pembatas

cat input.txt | tr " " "\n" | sort -n | tr "\n" " "

input: 1 2 4 1 4 32 18 3 keluaran:1 1 2 3 4 4 18 32


Anda dapat dengan aman menerima item numerik, dan ya: pembatas harus diganti.
Jeff Schaller
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.