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


11

У меня есть строка (или много строк) чисел, которые разделены произвольным символом. Какие инструменты UNIX я могу использовать для числовой сортировки элементов строки, сохраняя разделитель?

Примеры включают в себя:

  • список номеров; вход 10 50 23 42:; сортировать:10 23 42 50
  • Айпи адрес; вход 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
есть подобный вопрос также здесь, я хотел бы добавить его ссылку Алфавитные слова в именах файлов с помощью сортировки?
αғsнιη

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

Пожалуйста, уточните, находятся ли эти четыре примера DSV в одном файле или являются образцами из четырех разных файлов.
АРУ

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)

например для ,

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)'
Муру

@ 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'

Это нужно bash из-за использования <<<только. Если это заменить на here-doc, решение действительно для posix.
Это может сортировать поля с закладками, пробелами или оболочки Глобы символами ( *, ?, [). Не новые строки, потому что каждая строка сортируется.

Перейдите <<<"$2"на <"$2"обработку имен файлов и назовите их так:

shsort '.'    infile

Разделитель одинаков для всего файла. Если это ограничение, оно может быть улучшено.

Однако для обработки всего 6000 строк требуется 15 секунд. По правде говоря, оболочка не лучший инструмент для обработки файлов.

Awk

Для более чем нескольких строк (более нескольких десятков) лучше использовать настоящий язык программирования. Решение 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

Вот некоторый bash, который угадывает сам разделитель:

#!/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возвращается ошибка).


Это вдохновило это .
АРУ

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сценарий, который будет выполнять сортировку пузыря, но достаточно отличается, чтобы оправдать свой собственный ответ.

Разница в том, что вместо генерации 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

Идея сгенерированных регулярных выражений состоит в том, чтобы сопоставлять образцы для чисел, которые меньше, чем каждое целое число; эти два числа будут не в порядке, и поэтому поменяются местами. Регулярные выражения сгруппированы в несколько опций OR. Обратите пристальное внимание на диапазоны, добавленные к каждому элементу, иногда они есть {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.