Як можна чисельно сортувати один рядок з розмежуваними предметами?


11

У мене є рядок (або багато рядків) чисел, які розмежовані довільним символом. Які інструменти UNIX я можу використовувати, щоб сортувати елементи кожного рядка чисельно, зберігаючи роздільник?

Приклади включають:

  • список номерів; вхід 10 50 23 42:; відсортовано:10 23 42 50
  • IP-адреса; вхід 10.1.200.42:; відсортовано:1.10.42.200
  • CSV; вхід 1,100,330,42:; відсортовано:1,42,100,330
  • розмежовані на труби; вхід 400|500|404:; відсортовано:400|404|500

Оскільки роздільник є довільним, не соромтеся надати відповідь (або розширити) відповідь, використовуючи однозначний роздільник, який ви обрали.


8
ви повинні опублікувати його на codegolf :)
ivanivan

1
є подібне запитання і тут я хотів би додати його посилання Алфавітними словами в межах імен файлів за допомогою сортування?
αғsnιη

Просто натяк, який cutпідтримує довільні роздільники з його -dможливістю.
Олег Лобачов

Будь ласка, уточніть, чи є ці чотири приклади DSV s в одному файлі чи це зразки з чотирьох різних файлів.
agc

2
Побачивши деякі інші коментарі: роздільник є довільним, але його слід використовувати послідовно. Припустимо розвідку з боку виробника даних таким чином, щоб вони не використовували коми як роздільник і в даних (наприклад, 4,325 comma 55 comma 42,430не виникало б і не 1.5 period 4.2).
Джефф Шаллер

Відповіді:


12

Ви можете досягти цього за допомогою:

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

замініть крапки . з роздільником.
додайте -uдо sortкоманди вище, щоб видалити дублікати.


або за допомогою gawk( GNU awk ) ми можемо обробити багато рядків, тоді як вищезазначене також можна продовжити:

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

замініть *як роздільник поля SEP='*'своїм роздільником .


Примітки.
Можливо, вам потрібно буде використовувати -g, --general-numeric-sortопцію, sortа -n, --numeric-sortне обробляти будь-який клас чисел (ціле число, плаваючий, науковий, шістнадцятковий тощо).

$ 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

У awkвідсутності змін потреби, вона все одно буде обробка тех.


10

Використання perlочевидної версії; розділіть дані, відсортуйте їх, приєднайте їх знову.

Розмежувач потрібно вказати двічі (один раз у splitта один раз у join)

наприклад для a ,

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

Тому

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

Оскільки splitсимволом є регулярний вираз, символу може знадобитися цитування:

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

Використовуючи параметри -aі та -F, можна видалити розкол. За допомогою -pциклу, як і раніше, встановіть результати $_, які автоматично надрукують:

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

4
ви можете використовувати -lопцію, а не використовувати chomp. Це також додає нову лінію після друку. Дивіться також -a-F) для розділової частини.
Стефан Шазелас

1
З -lі -F, ще приємніше:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
muru

@ StéphaneChazelas дякує за -lваріант; Я пропустив це!
Стівен Харріс

1
@muru Я не використовував -Fпрапор спочатку, тому що він не працює належним чином у всіх версіях (наприклад, ваша лінія в CentOS 7 - perl 5.16.3 - повертає порожній вихід, хоча він добре працює на Debian 9). Але в поєднанні з -pцим дає дещо менший результат, тому я додав це як альтернативу відповіді. показує, як -Fможна використовувати. Спасибі!
Стівен Харріс

2
@StephenHarris це тому, що новіші версії perl автоматично додають -aі -nпараметри, коли -Fвикористовується і -nколи -aвикористовується ... так що просто перейдіть -leна-lane
Sundeep

4

Використання Python та аналогічну ідею, як у відповіді Стівена Харріса :

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>

Тож щось на кшталт:

$ 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

На жаль, робити введення-виведення вручну робить це набагато менш елегантним, ніж версія Perl.


3

Сценарій Bash:

#!/usr/bin/env bash

join_by(){ local IFS="$1"; shift; echo "$*"; }

IFS="$1" read -r -a tokens_array <<< "$2"
IFS=$'\n' sorted=($(sort -n <<<"${tokens_array[*]}"))
join_by "$1" "${sorted[@]}"

Приклад:

$ ./sort_delimited_string.sh "." "192.168.0.1"
0.1.168.192

На основі


3

Оболонка

Завантаження мови вищого рівня потребує часу.
Для кількох рядків сама оболонка може бути рішенням.
Ми можемо використовувати зовнішню команду sortта команду tr. Один досить ефективний при сортуванні ліній, а інший ефективний для перетворення одного роздільника в нові рядки:

#!/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'

Це потрібно баш через використання <<<тільки. Якщо це замінено на тут-doc, рішення дійсне для posix.
Це може сортувати поля з закладками, пробілами або оболонки Глоби символами ( *, ?, [). Не нові рядки, тому що кожен рядок сортується.

Змініть, <<<"$2"щоб <"$2"обробити назви файлів і назвіть це так:

shsort '.'    infile

Розмежувач однаковий для всього файлу. Якщо це обмеження, воно може бути покращене.

Однак обробка файлу, що містить лише 6000 рядків, займає 15 секунд. Дійсно, оболонка - не найкращий інструмент для обробки файлів.

Awk

Для більш ніж декількох рядків (більше кількох 10-х) краще використовувати справжню мову програмування. Рішенням awk може бути:

#!/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

Що займає всього 0,2 секунди для того самого файлу 6000 рядків, згаданого вище.

Зрозумійте, що <"$2"для файлів можна змінити назад <<<"$2"для рядків всередині змінних оболонок.

Perl

Найшвидше рішення - 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'

Якщо ви хочете сортувати зміну файлу <<<"$a"просто "$a"та додайте -iдо параметрів perl, щоб зробити видання файлу "на місці":

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

perlsort '.' infile; exit

2

Використовується sedдля сортування октетів IP-адреси

sedне має вбудованої sortфункції, але якщо ваші дані є досить обмеженими в діапазоні (наприклад, з IP-адресами), ви можете створити сценарій sed, який вручну реалізує простий сортування міхура . Основний механізм - пошук сусідніх номерів, які вийшли з ладу. Якщо цифри вийшли з ладу, поміняйте їх місцями.

Сам sedскрипт містить дві команди пошуку та заміни для кожної пари номерів поза порядком: по одній для перших двох пар октетів (змушує присутній кінцевий роздільник для позначення кінця третього октету) та друга для третьої пари октетів (закінчуємо EOL). Якщо відбувається заміна, програма відгалужується до верхньої частини скрипту, шукаючи номери, які вийшли з ладу. Інакше він виходить.

Створений сценарій частково:

$ 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

Цей підхід важко кодує період як роздільник, який слід уникати, оскільки в іншому випадку він був би "особливим" для синтаксису регулярного вираження (дозволяючи будь-який символ).

Для створення такого сценарію sed цей цикл виконає:

#!/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'

Скажімо, перенаправляйте вихід цього сценарію на інший файл, скажімо sort-ips.sed.

Зразок запуску може виглядати так:

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

Наступна варіація сценарію, що генерує, використовує маркерні позначення слова \<та \>позбавляється від необхідності другої заміни. Це також скорочує розмір створеного сценарію з 1,3 Мбайт до трохи менше 900 КБ, а також значно скорочує час роботи самого sedсебе (приблизно до 50% -75% від оригіналу, залежно від того sed, яка програма використовується):

#!/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
Цікава ідея, але, схоже, трохи ускладнює справи.
Метт

1
@Matt Це якось справа. Сортувати що-небудь із sed- це смішно, тому це цікавий виклик.
Кусалаланда

2

Ось дещо баш, який сам відгадує роздільник:

#!/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

Це може бути не дуже ефективно чи чисто, але це працює.

Використовуйте як bash my_script.sh "00/00/18/29838/2".

Повертає помилку, коли один і той же роздільник не використовується послідовно або коли два або більше роздільники слідують один за одним.

Якщо використаний роздільник є спеціальним символом, то він уникає (інакше sedповертає помилку).


Це надихнуло це .
agc

2

Ця відповідь ґрунтується на нерозумінні питання Q, але в деяких випадках це все-таки є правильним. Якщо вхід є цілком натуральними числами і має лише один роздільник на рядок (як у вибіркових даних у Q.), він працює правильно. Він також буде обробляти файли рядками, у кожного з яких є свій роздільник, що трохи більше, ніж просили.

Ця функція оболонки readз від стандартного введення, використовує підстановку параметрів POSIX , щоб знайти конкретний роздільник на кожному рядку, (збережений в $d), а також використовує , trщоб замінити $dз нового рядка \nі sort˙s дані тієї лінії, а потім відновлює початкові роздільники кожного рядка в:

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 ; }

Застосовується до даних, наведених в ОП :

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

Вихід:

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

Розмежувач у будь-якому рядку буде послідовним; загальні рішення, які дозволяють користувачу оголосити роздільник, є приголомшливими, але відповіді можуть вважати будь-який роздільник, який має для них сенс (одиничний символ і не присутній у самих числових даних).
Джефф Шаллер

2

Для довільних роздільників:

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

На вході, як:

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

Це дає:

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

0

Це має обробляти будь-який нецифровий роздільник (0-9). Приклад:

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/"

Вихід:

1!2!3!4!5

0

З 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

З ruby, який дещо схожий наperl

$ # -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


Спеціальна команда та проходження лише рядка-роздільника (не регулярного вираження). Буде працювати, якщо вхід має також плаваючі дані

$ # 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


Спеціальна команда для 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


Подальше читання - я вже мав цей зручний список перламутрових рублів perl / ruby


0

Далі йде зміна відповіді Джеффа в тому сенсі, що він генерує sedсценарій, який буде робити сортування Bubble, але достатньо інший, щоб гарантувати власну відповідь.

Різниця полягає в тому, що замість генерації O (n ^ 2) основних регулярних виразів це породжує O (n) розширені регулярні вирази. Отриманий сценарій буде розміром близько 15 Кб. Час запуску sedсценарію - частки секунди (для створення сценарію потрібно трохи більше часу).

Він обмежений сортуванням натуральних чисел, розмежених крапками, але це не обмежується розміром цілих чисел (просто збільшення 255основного циклу) або кількістю цілих чисел. Розмежувач можна змінити, змінивши delim='.'код.

Це зроблено моєю головою, щоб зрозуміти регулярні вирази, тому я залишу описувати деталі ще на один день.

#!/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'

Сценарій буде виглядати приблизно так:

$ 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

Ідея генерованих регулярних виразів полягає в узгодженні зразків для чисел, менших за кожне ціле число; ці два числа були б поза порядком, і тому вони замінені. Регулярні вирази згруповані в кілька варіантів АБО. Зверніть пильну увагу на діапазони, що додаються до кожного елемента, іноді вони є {0}, тобто попередній елемент слід опустити з пошуку. Параметри регулярного вираження зліва направо відповідають номерам, меншим за вказане число на:

  • ті місця
  • десятки місце
  • сотні місця
  • (продовжуйте за потребою для більшої кількості)
  • або меншою за величиною (кількість цифр)

Щоб прописати приклад, візьміть 101(з додатковими пробілами для читабельності):

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

Тут перше чергування дозволяє числам 100 на 100; друге чергування дозволяє від 0 до 99.

Інший приклад 154:

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

Тут перший варіант дозволяє від 150 до 153; другий дозволяє від 100 до 149, а останній дозволяє від 0 до 99.

Тестування чотири рази в циклі:

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

Вихід:

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

Розбиття вводу на кілька рядків

Використовуючи tr, ви можете розділити вхід, використовуючи довільний роздільник на кілька рядків.

Потім цей вхід можна пропустити sort(використовуючи, -nякщо вхід чисельний).

Якщо ви хочете зберегти роздільник у висновку, ви можете використовувати його trзнову, щоб додати роздільник.

наприклад, використовуючи простір як роздільник

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

вхід: 1 2 4 1 4 32 18 3 вихід:1 1 2 3 4 4 18 32


Ви можете сміливо вважати числові елементи, і так: роздільник слід замінити.
Джефф Шаллер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.