関連配列があるとしましょうbash
。
declare -A hash
hash=(
["foo"]=aa
["bar"]=bb
["baz"]=aa
["quux"]=bb
["wibble"]=cc
["wobble"]=aa
)
キーと値の両方がわからない場合(実際のデータは外部ソースから読み取られます)
すべての一意の値に対してループで実行できるように、同じ値に対応するキー配列をどのように作成できますか?
printf 'Value "%s" is present with the following keys: %s\n' "$value" "${keys[*]}"
出力を取得します(必ずしもこの順序である必要はありません)。
Value "aa" is present with the following keys: foo baz wobble
Value "bb" is present with the following keys: bar quux
Value "cc" is present with the following keys: wibble
重要なのは、キーが配列keys
に別々の要素として格納されるため、テキスト文字列から解析する必要がないことです。
私は次のことをすることができます
declare -A seen
seen=()
for value in "${hash[@]}"; do
if [ -n "${seen[$value]}" ]; then
continue
fi
keys=()
for key in "${!hash[@]}"; do
if [ "${hash[$key]}" = "$value" ]; then
keys+=( "$key" )
fi
done
printf 'Value "%s" is present with the following keys: %s\n' \
"$value" "${keys[*]}"
seen[$value]=1
done
しかし、二重ループの効率はやや低いようです。
配列構文がありませんbash
か?
たとえば、これにより、zsh
より強力な配列操作ツールが提供されますか?
Perlではやる
my %hash = (
'foo' => 'aa',
'bar' => 'bb',
'baz' => 'aa',
'quux' => 'bb',
'wibble' => 'cc',
'wobble' => 'aa'
);
my %keys;
while ( my ( $key, $value ) = each(%hash) ) {
push( @{ $keys{$value} }, $key );
}
foreach my $value ( keys(%keys) ) {
printf( "Value \"%s\" is present with the following keys: %s\n",
$value, join( " ", @{ $keys{$value} } ) );
}
しかし、bash
連想配列は配列を保持できません...
私はまた、ある種の間接索引付け(上記の値を読みながらインデックス配列を構築すること)を使用できる古いソリューションにも興味がありますhash
。線形時間内にこれを行う方法があるはずです。
答え1
扱いにくい
キー<=>値の反転
でzsh
ハッシュを定義する主な構文は次のとおりですhash=(k1 v1 k2 v2...)
(perl
最新バージョンではキー参照時の変更はありますが、互換性のために厄介なksh93 / bash構文もサポートしています)。
keys=( "${(@k)hash}" )
values=( "${(@v)hash}" )
typeset -A reversed
reversed=( "${(@)values:^keys}" ) # array zipping operator
または、Oa
パラメータ拡張フラグを使用して、キーと値のリストの順序を逆に置き換えます。
typeset -A reversed
reversed=( "${(@kvOa)hash}" )
またはループを使用してください。
for k v ( "${(@kv}hash}" ) reversed[$v]=$k
二重引用符は、@
空のキーと値を保持するためのものです(bash
連想配列は空のキーをサポートしていません)。連想配列の要素は特定の順序で拡張されないため、複数の要素が$hash
同じ値(最終的にはキーになる$reversed
)を持つ場合、どのキーがの値として使用されるかはわかりません$reversed
。
あなたの周期のために
ハッシュ添え字フラグを使用R
して、キー以外の値に基づいて要素を取得し、e
(ワイルドカードではない)正確な一致と組み合わせてから、パラメータk
拡張フラグを使用してこれらの要素のキーを取得できます。
for value ("${(@u)hash}")
print -r "elements with '$value' as value: ${(@k)hash[(Re)$value]}"
あなたのPerlメソッド
zsh
配列の配列はサポートされていませんが(とは対照的にksh93
)、その変数にはNULバイトを含めることができるため、NULバイトを含まない要素を分離するために使用したり、参照を使用してリストをエンコード${(q)var}
/デコードしたりできます。${(Q)${(z)var}}
typeset -A seen
for k v ("${(@kv)hash}")
seen[$v]+=" ${(q)k}"
for k v ("${(@kv)seen}")
print -r "elements with '$k' as value: ${(Q@)${(z)v}}"
クッシュ 93
ksh93は1993年に連想配列を導入した最初のシェルでした。グローバル割り当ての構文は、これをプログラムで実行することは非常に困難であることを意味しますzsh
が、少なくともksh93はksh93
かなり合理的な複雑なネストされたデータ構造をサポートします。
具体的には、ksh93はハッシュ要素の値として配列をサポートしているため、次のことができます。
typeset -A seen
for k in "${!hash[@]}"; do
seen[${hash[$k]}]+=("$k")
done
for k in "${!seen[@]}"; do
print -r "elements with '$k' as value ${x[$k][@]}"
done
強く打つ
bash
連想配列のサポートは数十年後に追加され、ksh93構文はコピーされましたが、他の高度なデータ構造はコピーされず、zshの高度なパラメータ拡散演算子もコピーされませんでした。
あなたはbash
使用することができます参考文献リストzshに記載されている方法を使用printf %q
するか、最新バージョンを使用してください${var@Q}
。
typeset -A seen
for k in "${!hash[@]}"; do
printf -v quoted_k %q "$k"
seen[${hash[$k]}]+=" $quoted_k"
done
for k in "${!seen[@]}"; do
eval "elements=(${seen[$k]})"
echo -E "elements with '$k' as value: ${elements[@]}"
done
ただし、前述のように、連想配列はnull値をキーとしてサポートしていないため、一部の値がnullの場合、連想配列はbash
機能しません。たとえば、空の文字列をいくつかのプレースホルダに置き換えるか、後で表示するために削除するキーの前にいくつかの文字を追加することを$hash
選択できます。<EMPTY>
答え2
ご存知のように、障害は、インデックス配列の名前を(他の)変数の値として使用するときにインデックス配列の合計値を取得することです。中間値より小さい値でこれを実行して${v[@]}
からevalを使用することはできません。したがって、アプローチは次のようになります。
declare -A keys
N=0 # counter for the index variables IX1, IX2, IX3, ...
for key in "${!hash[@]}"; do
value="${hash[$key]}"
if [ -z "${keys[$value]}" ] ; then N=$((N+1)) ; keys[$value]=IX$N ; fi
index="${keys[$value]}" # 'index' is now name of index variable
X="\${$index[@]}"
eval "$index=( $X $key )" # adding next key to it
done
for value in "${!keys[@]}" ; do
index=${keys[$value]}
X="\${$index[@]}"
printf "Value %s is present with the following keys: %s\n" \
"$value" "$(eval echo "$X")"
done
これはLinux用ですbash
。見つかったさまざまな値などのインデックス配列を作成し、IX1
そのIX2
名前をkeys
値の関連配列に保存します。したがって、${keys[$value]}
この値のキーを保持するインデックス配列の名前です。その後、X
値のコレクションに設定された変数「アクセスフレーズ」は、スペースで区切られeval echo "$X"
た値に変換できるように設定されます。たとえば、値に配列インデックスがある場合は文字列にIX2
なります。X
${IX2[@]}
zsh
配列の配列をサポートしていないという点で似ていると思うので、同様の解決策が必要になるかもしれません。 IMHOアクセスフレーズがzsh
もう少し明確になりました。
答え3
別のアプローチは、2つのインデックス配列にデータを格納することです。そのうちの1つは一意の値を持ち、2番目の値には重複/重複値を含めることができます。 2番目の配列で繰り返される要素はキーで、最初の配列の対応する項目はスペースで区切られた値で連想配列を構成できます。
以下のコードはループの使用を避け、ループeval
のみを使用します。for
パスワード
source=("foo" "bar" "baz" "quux" "wibble" "wobble")
destination=("aa" "bb" "aa" "bb" "cc" "aa")
declare -A inverted_array
# Printout formatted arrays with headers
printf '%-10s %-20s %-30s\n' "Index" "Destination" "Source"
for ((i = ((${#source[@]} - 1)); i >= 0; i--)); do
source_i="${source["$i"]}"
destination_i="${destination["$i"]}"
printf '%-10s %-20s %-30s\n' "$i" "$destination_i" "$source_i"
tempstring="${inverted_array["$destination_i"]}"
inverted_array["$destination_i"]="$source_i"" ""$tempstring"
done
echo
printf '%-10s %-20s\n' "Key" "Value"
# Remove the last space from the every element of the resulted array and print it formatted
for index in "${!inverted_array[@]}"; do
removespace="${inverted_array[$index]}"
removespace=${removespace%" "}
inverted_array["$index"]="$removespace"
printf '%-10s %-20s\n' "$index" "${inverted_array["$index"]}"
done
echo
出力:
Index Destination Source
5 aa wobble
4 cc wibble
3 bb quux
2 aa baz
1 bb bar
0 aa foo
Key Value
bb bar quux
aa foo baz wobble
cc wibble
PS上記の例をさらに拡張/デモンストレーションするために、以下に2つの配列を生成するコードがあります。そのうちの1つにはsource
5文字の長さのランダム文字が含まれ、2番目にはdestination
値としてランダムな文字0-9a-fのみが含まれています。
それぞれ100個の要素で構成される2つのインデックス配列を生成するコード:
for ((i = 0; i < 100; i++)); do
source+=("$(tr -dc 'a-zA-Z' </dev/urandom | head -c 5)")
destination+=("$(tr -dc '0-9a-f' </dev/urandom | head -c 1)")
done
上記のコードを使用して連想配列を生成すると、結果は次のようになります。
Key Value
9 soxRg PmUZv eOmkR cFuie wmlsO EdNdM XuloF SSfjE oHfnc FcIKE
8 hLRpa eXODM wRGkh MwZUW lfWaE WQiwU IHGjj nNEcg
7 Pdxmd ywPZQ lPQIx TKawd VTyqR
6 lIwla Docxu Dimnz ovywP HwzQv
5 ObezH tyFNS BqnWp CFlMk dDkYC
4 rNzLM GVLXH AgZSL ionEp tngzQ
3 yRfqn IdTne
2 sMSxm WKmGm ELjOL pqxqw stWnL
1 yxycd EAGRg WxBle ItLNz WUdVu shUaC qDNIO xIwdM
0 OXdHh VQcsT AFvFq sgrYK AQrjZ
f uXJor IkwDr AOGSK hYMGE PQQfu tUjbh NwrVi iqZKO hHLYU
e XhMpB TCCFr ATbxa
d ReqMh lbxFx bGivd YCGtv lAtZj
c Kvthr itbaF wIbaf LwUiB VTInv xvWbC gpyRZ
b riimt EkLbv QYpZq kgvTi tOJRH jZykW pRuMD FJVXZ xipDx wkCMN
a REJnb Xtunv raimk SemnZ xMwno EXwKi sekmg WUKhx
答え4
使用幸せ(以前のPerl_6)
my %hash = (
'foo' => 'aa',
'bar' => 'bb',
'baz' => 'aa',
'quux' => 'bb',
'wibble' => 'cc',
'wobble' => 'aa'
);
my %inverted = %hash.classify( { .value }, :as{ .key } );
for %inverted.kv -> $k, $v {
printf( "Value \"%s\" is present with the following keys: %s\n",
$k, $v ) };
出力:
Value "aa" is present with the following keys: wobble baz foo
Value "bb" is present with the following keys: bar quux
Value "cc" is present with the following keys: wibble
簡単に言えば、ここでの作業の鍵は、コンポーネントに基づいて要素をテストし、等価値を分類するRakuのclassify
ルーチンを使用して行うことです。%hash
.value
:as
.key
ほとんどのタスクを実行する1行のコードは次のとおりです(Raku REPLで実行可能)。
.say for %hash.classify: {.value}, :as{.key};
cc => [wibble]
aa => [baz wobble foo]
bb => [quux bar]
classify
付録(1):Rakuにも同様の機能がありますcategorize
。上記のコードでも同じ結果classify
に置き換えることができます。categorize
%hash
付録(2):元のオブジェクトを再構成するには、そのルーチンを%inverted
呼び出すだけですinvert
。文書によると:invert
「反転と反転の違いは、antipairs
リスト値を複数のペアに拡張することです。」
https://docs.raku.org/routine/classify
https://docs.raku.org/routine/categorize
https://docs.raku.org/routine/invert
https://raku.org