如何使用 powershell 将包含 2 个数字的字符串转换为货币?

How to convert a string containing 2 numbers to currency with powershell?

我的文本文件包含 2 个由“+”号分隔的数字。想弄清楚如何用等值货币替换它们。
示例字符串:

20+2 would be converted to [=12=].20+[=12=].02 USD

1379+121 would be> .79+.21 USD

400+20 would be .00+[=12=].20 USD

等等。

我试过使用几个角度,但它们不起作用或提供奇怪的结果。 我试图通过尝试找到我认为会出现的所有模式来做到这一点。

.\Replace-FileString.ps1 "100+10" '.00+[=10=].10' $path1\*.txt -Overwrite
.\Replace-FileString.ps1 "1000+100" '.00+.00' $path1\*.txt -Overwrite
.\Replace-FileString.ps1 "300+30" '.00+[=10=].30' $path1\*.txt -Overwrite
.\Replace-FileString.ps1 "400+20" '.00+[=10=].20' $path1\*.txt -Overwrite

或者这个根本行不通。

Select-String -Path .\*txt -Pattern '[0-9][0-9]?[0-9]?[0-9]?[0-9]?\+[0-9][0-9]?[0-9]?[0-9]?[0-9]?' | ForEach-Object  {$_ -replace ", ", $"}  {$_ -replace "+", "+$"}

I tried to do it here by attempting to find by all patterns I think would come up

不要尝试这个 - 我们是人类,我们不会考虑所有边缘情况,即使我们考虑了,我们需要编写(或生成)的代码量也将是荒谬的。


我们在这里需要一个更通用的解决方案,正则表达式可能确实对此有所帮助。

您描述的模式可以表示为三个不同的部分:

  1. 1 个或多个连续数字
  2. 1 个加号 (+)
  3. 1 个或多个连续数字

考虑到这一点,让我们开始简化要使用的正则表达式模式:

\b\d+\+\d+\b

或者,写出解释:

\b       # a word boundary
  \d+    # 1 or more digits
  \+     # 1 literal plus sign
  \d+    # 1 or more digits
\b       # a word boundary

现在,为了将美分的绝对值转换为美元,我们需要捕获 + 两边的数字,所以让我们添加捕获组:

\b(\d+)\+(\d+)\b

现在,为了对捕获的组做任何有趣的事情,我们可以利用 Regex.Replace() method - 它可以将脚本块作为其替换参数:

$InputString  = '1000+10'
$RegexPattern = '\b(\d+)\+(\d+)\b'
$Substitution = {
    param($Match)

    $Results = foreach($Amount in $Match.Groups[1,2].Value){
        $Dollars = [Math]::Floor(($Amount / 100))
        $Cents   = $Amount % 100
        '${0:0}.{1:00}' -f $Dollars,$Cents
    }
    return $Results -join '+'
}

在上面的脚本块中,我们期望两个捕获组($Match.Groups[1,2]),计算美元和美分的数量,然后最后使用 -f 字符串格式运算符来确保美分值总是两位数宽。

要进行替换,请调用 Replace() 方法:

[regex]::Replace($InputString,$RegexPattern,$Substitution)

好了!

应用于一堆文件非常简单:

$RegexPattern = '\b(\d+)\+(\d+)\b'
$Substitution = {
    param($Match)

    $Results = foreach($Amount in $Match.Groups[1,2].Value){
        $Dollars = [Math]::Floor(($Amount / 100))
        $Cents   = $Amount % 100
        '${0:0}.{1:00}' -f $Dollars,$Cents
    }
    return $Results -join '+'
}

foreach($file in Get-ChildItem $path *.txt){
    $Lines = Get-Content $file.FullName
    $Lines |ForEach-Object {
        [regex]::Replace($_, $RegexPattern, $Substitution)
    } |Set-Content $file.FullName
}

你想要这样的输出吗?

+ would be converted to [=10=].20+[=10=].02 USD

79+1 would be> .79+.21 USD

0+ would be .00+[=10=].20 USD

然后,您可以在 powershell 中尝试此命令。

(gc test.txt) -replace '\b(\d+)\+(\d+)\b','$$+$$' | sc test.txt
  • gc , sc : get-content, set-content 命令的别名
  • \b(\d+)\+(\d+)\b : 匹配目标字符串(numbers+numbers)并抓取数字到</code>, <code>顺序
  • $$ : $ 必须转义表示 literal $ dollor character (numbers前面要放什么)
  • , : back-reference 到捕获值
  • test.txt :包含您的示例文本

当然,这适用于像下面这样的多个文件

gci '*.txt' -recurse | foreach-object{(gc $_ ) '\b(\d+)\+(\d+)\b','$$+$$' | sc $_  }
  • gci : get-childitem 命令的别名。默认情况下,它 returns 在当前目录中列出。如果要更改目录,则必须使用 -path 选项和 -include 选项。
  • -recurse 选项:启用搜索 sub-directory

已编辑

如果你想要 capturing & dividing values & replacing 旧值与新值如下

[=13=].2+[=13=].02 would be converted to [=13=].20+[=13=].02 USD

.79+.21 would be> .79+.21 USD

+[=13=].2 would be .00+[=13=].20 USD

那你可以试试这个

gci *.txt -recurse | % {(gc $_) | % { $_ -match "\b(\d+)\+(\d+)\b" > $null; $num1=[int]$matches[1]/100; $num2=[int]$matches[2]/100; $dol='$$'; $_ -replace "\b(\d+)\+(\d+)\b","$dol$num1+$dol$num2"}|sc $_}

此命令在当前目录和 sub-directory 中搜索文件。如果您不想在 sub-directory 中搜索,请删除 -recurse 选项。如果您想要另一条路径,请使用 -path 选项和 -include 选项,如下所示。

gci -path "your_path" -include *.txt | % {(gc $_) ... 

这个正则表达式也有效

\b\d{3,4}(?=\+)|\d{2,3}(?=\")

https://regex101.com/

其他解决方案似乎过于复杂,先将字符串转为值,然后再转回字符串。查看示例,它只是切碎一个字符串并 re-assembling 它同时确保不同部分(美元和美分)具有正确的长度:

('20+2','1379+121','400+20') -replace
        '(\d+)\+(\d+)','00+00' -replace
        '0*(\d+)(\d\d)\+0*(\d+)(\d\d)','$$.+$$. USD'

[=10=].20+[=10=].02 USD
.79+.21 USD
.00+[=10=].20 USD

解释:

  1. 用 0 填充值替换所有 + 分隔的美分值,这样至少有三位数字,即美元至少有一位,美分恰好有 2 位。
  2. 将每个值的单个美元和美分收集到不同的捕获组中,同时丢弃任何无关的前导零。
  3. Re-substitute 具有适当格式版本的(刚刚填充的)字符串。

有趣的是,第二个替换是如何依赖于 * 的贪心性质的。 0* 将匹配尽可能多的前导零,但仍会为模式的其余部分留下足够的空间。

您可以在模式的一端或两端放入单词边界锚点 (\b),如果您的一行中有部分数字由 + 分隔并且与其他文本直接相邻并且您希望他们 处理,否则不需要。

注意:上面的示例显示了一个 String 的数组作为输入并生成了一个 String 的数组(每个元素显示在单独的行上)。当 -Replace 应用于数组时,它会枚举数组,将替换应用于每个元素并将每个(可能被替换的)元素收集到结果数组中。 Get-Content 的输出是 String 的数组(在提供管道时由 PowerShell 枚举)。类似地,'input' 到 Set-Content 是一个 String 的数组(可能是从管道输入收集的一般 Object[] and/or 转换而来)。因此,要转换文件只需使用:

(gc somefile) -replace ... -replace ... | sc newfile

# or even

sc newfile ((gc somefile) -replace ... -replace ...)

# Set-Content [-Path] String[] [-Value] Object[]

在上面,newfilesomefile 可以是相同的,因为 Set-Content 的一个很好的特性甚至 open/create 它的输出文件,直到它有东西要写。因此,

@() | sc existingfile

不破坏现有文件。但是请注意,

sc existingfile @()

确实会破坏 现有文件。这是因为第一个示例没有向 Set-Content 发送任何内容,而第二个示例向 [=​​24=] 发送了一些内容(一个空数组)。由于 Get-Content 的输出在应用 -Replace 之前被收集到一个(匿名)数组中,因此 Get-ContentSet-Content 之间在访问同一文件时没有冲突。功能等效的版本

gc somefile | foreach { $_ -replace ... -replace ... } | sc newfile
如果 newfilesomefile

不起作用,因为 Set-Content 从 [=20= 接收每一行(可能被替换) ] 在读取下一个之前意味着 Set-Content 无法打开文件,因为 Get-Content 仍然打开它。

这是一个单独的答案,因为它没有解释如何达到预期的结果(已经这样做了),而是解释了为什么列出的尝试不起作用(教育动机)。

如果您使用 GitHub 中的 Replace-FileString.ps1,那么这些示例不仅不是通用解决方案,而且不会像上面列出的那样工作,因为 Replace-FileString.ps1 使用 [= [regex] 对象的 14=] 方法,因此“400+20”匹配“40”,然后匹配 1 个或多个“0”,然后匹配“20”。其他尝试也类似。请注意,模式中没有匹配“+”,因此全部失败(除非您有像“40020+125”这样的行与 40020 匹配)。同样,替换包括捕获组说明符“$0”(作为“$1.00+$0.10”的一部分)和其他说明符。模式中没有指定捕获组,所以所有的组说明符都将按字面意思使用,除了“$0”是整个匹配项(如果找到的话)。因此,“40020+125”将被替换为“$4.00+$0.20”,给出“$4.00+40020.20”($4='$4' 和 $0='40020')。可能找不到匹配项。结果 -> 文件未更改。 (呸!)

至于 Select-String 尝试,Select-String 可能会匹配所需的数据,因为该模式最多匹配 + 两侧的 5 位数字。这会将匹配行(并忽略其余部分,如果有的话)作为 [Microsoft.PowerShell.Commands.MatchInfo] 对象(不是字符串)发送到 ForEach-Object 中。 (旁白:这是很多 PowerShell 新手常犯的错误。他们认为他们在屏幕上看到的内容与 PowerShell 内部正在搅动的内容是一样的。这与事实相去甚远,并且可能导致大多数新用户之间的混淆。PowerShell 处理整个对象并且通常只显示最有用位的摘要。)无论如何,我不确定 ForEach-Object 试图实现什么,尤其是由于明显的错字。第一个脚本块中至少缺少一个 ",可能还缺少一个逗号。我能解释的最好的是

{ $_ -replace ", ",", $" }

即将每个“,”更改为“,$”。这假定要替换的字符串都以“,”开头。注意:单独的 $ 不是错误,因为它不能解释为变量替换(没有后面的名称或 {)或捕获引用(没有后面的组说明符 [0-9`+'_&])。下一个脚本块更清晰,将每个“+”更改为“+$”。不幸的是,第一个字符串再次被解释为正则表达式,与单独的 $ 不同,单独的 + 在这里 一个错误。它需要用\转义。然而,即使纠正了这些错误,仍然存在两个大问题:

  1. Select-String 的默认输出是 [MatchInfo] 对象的集合,当(隐式)转换为 String 用作 -replace 的 LHS 包含文件名称和行号,从而破坏文件中的行。要仅使用行本身,请指定 $_.Line.
  2. ForEach-Object 的脚本块参数的用法完全不正确。虽然看起来意图是执行两个替换操作,但将它们放在单独的脚本块中是错误的。即使有效,它也会输出 2 个单独的部分替换而不是一个完整的替换,因为 $_ 在两个表达式之间没有更新。 ($_ 可写!)

ForEach-Object有3个基本脚本块组,1个-Begin块,1个-End块,其余统称为-Process块。 (-Parallel 块在这里不相关。)文档提到了一个名为 -RemainingScripts 的组,但这实际上只是一个实现结构,允许将 -Process 脚本块指定为单独的参数,而不是收集到一个数组中(类似于 C# 和 VB 中的参数数组)。我怀疑这样做是为了让用户可以简单地删除参数名称(-Begin、-Process 和 -End)并将脚本块视为位置参数,尽管严格来说,只有 -Process 是位置参数并且期望一组脚本块(即用逗号分隔)。在 PS3.0 中引入 -RemainingScripts(具有属性 ValueFromRemainingArguments,因此它的行为类似于参数数组)可能是为了整理可能令人讨厌的内容,让用户友好PS3.0 之前的行为。或者它可能只是将已经发生的事情正式化。

总之,回到主题。通过指定多个脚本块,第一个被视为 -Begin,如果超过 2 个,最后一个被视为 -End。因此,对于两个脚本块,第一个是 -Begin,另一个是 -Process。因此,即使第一个脚本块在语法上是正确的,它也只会 运行 一次,然后仍然什么都不做,因为 -Begin 中没有分配 (=$null) $_。正确的方法是在一个脚本块中放置两个替换项,连接成一个表达式:

{ $_.Line -replace ", ",", $" -replace "\+","+$" }

当然,这只是描述如何让它“工作”。这不是解决问题的正确方法n 原文post(见其他答案)。