将逗号分隔的数字列表转换为连字符列表或范围

将逗号分隔的数字列表转换为连字符列表或范围

我有一个逗号分隔的数字字符串,如下所示:

1,2,3,5,6,7,8,9,12,14

我正在寻找一个在bash脚本中使用的命令,该命令可以将相邻数字组合成范围/连字符条目,如下所示:

1-3,5-9,12,14

保证初始字符串按升序排序。

答案1

使用perl:

perl -pe 's/\b(\d+)(?{$q=$1+1})(?:,(??{$q})\b(?{$p=$q++})){2,}/$1-$p/g'

(?{...})这是通过and表达式使用嵌入 Perl 代码的正则表达式(??{...});第一个仅评估嵌入代码,而第二个则使用它返回的值作为模式。请参阅perlre(1)参考资料 获取完整描述。

如果您还想要仅包含两个数字的范围(例如-> ),请将量词替换{2,}为。+1,2,71-2,7

答案2

这是一个简短的awk脚本,它遍历以逗号分隔的已排序整数列表,并在执行此操作时填充两个数组a和。b

a数组将包含每个单调递增整数范围的起始整数,同时b包含相应的结束整数。n代码中的变量保存找到的范围的数量。

BEGIN {
    OFS = FS = ","
}

{
    n = 0

    a[++n] = $1
    for (i = 1; i < NF; ++i)
        if ($i != $(i+1) - 1) {
            b[n] = $i
            a[++n] = $(i+1)
        }
    b[n] = $NF

    $0 = ""

    for (i = 1; i <= n; ++i)
        if (a[i] == b[i])
            $i = a[i]
        else
            $i = sprintf("%d-%d", a[i], b[i])

    print
}

通过迭代找到的不同范围并构造一条记录来创建输出n,其中每个字段要么是单个整数(对于长度为 1 的范围),要么是表示范围开始和结束的字符串。

对您提供的数据进行测试,从文件中读取数据:

$ awk -f script.awk file
1-3,5-9,12,14

显然,您可以使用标准输入中的字符串来提供它,如下所示:

$ awk -f script.awk <<<"1,2,3,5,9,10,11,12,13"
1-3,5,9-13

答案3

相反zsh,您可以定义一个函数,例如:

reduce() {
  local i=1
  argv=(${(nus:,:)1}) # split $1 on ",", numerically sort and remove dups
  while ((i < $#)) {
    if ((${argv[i]#*-} + 1 == ${argv[i+1]%-*})) {
      argv[i]=${argv[i]%-*}-${argv[i+1]#*-}
      argv[i+1]=()
    } else {
      ((i++))
    }
  }
  print ${(j:,:)@}
}

这也将接受输入范围:

$ reduce 1,2,3,5,6,7,8,9,12,14
1-3,5-9,12,14
$ reduce 1,2,3,5-7,8,9-11,12,13-20
1-3,5-20
$ reduce 5,2,4,5,6
2,4-6

请注意,如果输入范围重叠,它将无法正常工作:

$ reduce 1-3,2
1-3,2
$ reduce 1-3,2-4
1-3,2-4

bash,您可以将函数定义为:

reduce() { zsh -c '
  i=1
  argv=(${(nus:,:)1}) # split $1 on ",", numerically sort and remove dups
  while ((i < $#)) {
    if ((${argv[i]#*-} + 1 == ${argv[i+1]%-*})) {
      argv[i]=${argv[i]%-*}-${argv[i+1]#*-}
      argv[i+1]=()
    } else {
      ((i++))
    }
  }
  print ${(j:,:)@}' zsh "$@"
}

答案4

我最终得到了以下 BASH 函数,使用数组来承载已识别的范围。输入字符串是函数的第一个参数,结果通过第二个参数传回:

function compact_range {
  arr=()
  start=""
  for cpu in ${1//,/ }; do
    # Start a new range definition if necessary
    [ -z "$start" ] && start=$cpu && range=$cpu && last=$cpu && continue
    prev=$(( $cpu - 1 ))
    # If the current CPU is not adjacent to the last CPU, start a new range
    [ "$prev" -ne "$last" ] && arr+=($range) && start=$cpu && range=$cpu && last=$cpu && continue
    # Current CPU is adjacent to an existing range, expand the current range
    range="${start}-${cpu}" && last=$cpu
  done
  # Append the last range to the array of ranges
  arr+=($range)
  # Return a comma delimited list of ranges
  eval $2=$(IFS=,;printf "%s" "${arr[*]}")
}

感谢大家的想法。

相关内容