Following up from this question, I'm trying to replace $B$1 to TEXT($B$1,"0000") on all formulas I can find on a lot of workbooks. Now that i'm past that .save() problem, I've got another (which should've been the first, actually): I can't seem to change .Formula value, no matter what I try.
PS C:\> $Search.Formula = $Search.Formula -replace '\$B\$1','TEXTO($B$1,"0000")'
Exceção ao definir "Formula": "Exceção de HRESULT: 0x800A03EC"
No linha:1 caractere:1
+ $Search.Formula = $Search.Formula -replace '\$B\$1','TEXTO($B$1,"0000")'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], SetValueInvocationException
+ FullyQualifiedErrorId : CatchFromBaseAdapterSetValueTI
Formula is:
=PROCV(("A"&ANO($A6)&"M"&MÊS($A6)&"P"&$B$1);BASE!$A:$P;9;FALSO)
In English, if I remember the correlation correctly:
=VLOOKUP(("A"&YEAR($A6)&"M"&MONTH($A6)&"P"&$B$1);BASE!$A:$P;9;FALSE)
The expected output would be
=VLOOKUP(("A"&YEAR($A6)&"M"&MONTH($A6)&"P"&TEXT($B$1,"0000"));BASE!$A:$P;9;FALSE)
There were a couple of things going on with what you supplied. At first glance, you seem to be using the backslash for an escape character to make the dollar signs literal. The escape character for this in PowerShell is the back-tick or grave (e.g. `).
If I was performing this action within Excel, I would probably just Find & Replace every $B$1 on the worksheet with text($B$1, "0000"). Seems to me that it is powerful enough to take care of the operation without PowerShell's -replace method. The worksheet method does depend somewhat on $B$1 begin available but since it is also in the replacement, you pretty much need to know what you are replacing beforehand. Some error control in that area may be necessary if this script is left for casual users.
$excel = New-Object -comobject Excel.Application
$FilePath = "c:\temp\example.xlsx"
$workbook = $excel.Workbooks.Open($FilePath)
$excel.Visible = $true
$worksheet = $workbook.worksheets.item("Sheet1")
#set some Find & Replace vars
$what = "`$B`$1"
$with = "text(`$B`$1, `"0000`")"
#use worksheet-wide Find & Replace to change formula
$worksheet.usedrange.replace($what, $with, 2)
#formula(s) should be changed. now Find it and display it
$fnd = $worksheet.usedrange.find($what, $worksheet.range("A1"), -4123, 2)
Write-Output $fnd.formula
$workbook.save()
$workbook.close()
$excel.quit()
I've proofed the Range.Replace method by finding and displaying the formula after the operations and made more extensive use of the grave escape character rather than swap back and forth between single quotes and double quotes within quoted strings.
The above code uses the EN-US version I tested with. The actual replacement text for your regional settings would seem to be,
$with = "texto(`$B`$1; `"0000`")"
Related
I have an Excel file that I receive and want to process it to a CSV using Powershell.
I have to alter it quite specifically so it can be a reliable input for a program that will process the csv info.
I don't know the exact headers, but i know there can be duplicates.
What I do is open the xlsx file with excel and save it as CSV:
$objExcel = New-Object -ComObject Excel.Application
$objExcel.Visible = $True
$objExcel.DisplayAlerts = $True
$Workbook = $objExcel.Workbooks.open($xlsx1)
$WorkSheet = $WorkBook.sheets.item($sheet)
$xlCSV = 6
$Workbook = $objExcel.Workbooks.open($xlsx2)
$WorkSheet = $WorkBook.sheets.item($sheet)
$WorkBook.SaveAs($csv2,$xlCSV)
Now, the XLSX file will have comma's, so first I want to change them to dots.
I tried this, but it's not working:
$objRange = $worksheet.UsedRange
$objRange.Replace ",", "."
It errors out saying: Unexpected token '", "'.
Then when saving I want to set the Delimiter to comma, as it uses ";" standard.
With something like:
$WorkBook.SaveAs($csv2,$xlCSV) -delimiter ","
The last problem is the duplicate headers; this prevents PS to use Import-CSV. Here I tried, when file is separated with a comma it works:
Get-Content $downloads\BBKS_DIR_AUTO_COMMA.csv -totalcount 1 >$downloads\Headers.txt
But then I need to rename de duplicate names like I can have Regio, Regio, Regio.
I want to change this to Regio, Regio2, Regio3
My plan was to lookup the data of the txt, search for duplicates, and then ad an incremental nummer.
In the end I need to add a column with incremental numbers, but always with four numbers, like; 0001, 0002, 0010, 0020, 0200, 1500, I wont exceed 9999. How can this be done?
If you can help me, if only partially I'm very happy.
Further, I'm running Windows 7 x64, Powershell 3.0, Excel 2016 (if relevant)
If easier, its fine to go back to Command prompt for some tasks.
Personally, I wouldn't try and work with Excel sheets via Excel itself and COM - I'd use the excellent module https://github.com/dfinke/ImportExcel
Then you can import from the sheet straight to a native Powershell object array, and re-export with Export-Csv -Delimiter.
Edit: To answer follow ups :
Once you've loaded the module you can do "Get-Module ImportExcel | Select-Object -ExpandProperty ExportedCommands" to see what it makes available.
To import your Excel in the first place, do something like :
$WorkBook = Import-Excel
And if you need to take care of duplicate column names, you can do :
$WorkBook = Import-Excel -Header #("Regio1", "Regio2", "Regio")
Where the array you pass to -Header needs to include every column you want from the workbook.
I have two computers, one with windows7 and one with windows10. Both computers use Excel 15.0.4753.1003.
The following script fails on Windows10:
function write-toexcelrange(){
param(
#The range should be a cell in the upper left corner where you want to "paste" your data
[ValidateNotNullOrEmpty()]
$Range,
# data should be in the form of a jagged multiarray ("row1Column1","row2column2"),("row2column1","row2column2")
# if data is a simple array of values, it will be interpreted as 1 column with multiple rows
# Rows can differ in length
[validatenotnullorempty()]
[array]$data
)
$rows=0
$cols=0
if($data -is [array]) {
foreach($row in $data){
$rows++
$cols=[math]::max($cols,([array]$row).length)
}
#Create multiarray
$marr=new-object 'string[,]' $rows,$cols
for($r=0;$r -lt $marr.GetLength(0);$r++) {
for($c=0;$c -lt $marr.GetLength(1);$c++) {
$marr[$r,$c]=[string]::Empty
}
}
for($r=0;$r -lt $rows;$r++) {
if($data[$r] -is [array]){
for($c=0;$c -lt ([array]$data[$r]).length;$c++) {
$marr[$r,$c]=$data[$r][$c].ToString()
}
} else {
$marr[$r,0]=$data[$r].ToString()
}
}
$wrr=$range.resize($rows,$cols)
$wrr.value2=$marr
} else {
$wrr=$range
$wrr.value2=$data
}
#Return the range written to
$wrr
}
$excel = New-Object -ComObject Excel.Application
$excel.visible = $true
$defaultsheets=$excel.SheetsInNewWorkbook
$excel.SheetsInNewWorkbook=1
$wb = $Excel.Workbooks.add()
$excel.SheetsInNewWorkbook=$defaultsheets
$mysheet = $wb.worksheets.item(1)
$mysheet.name = "test"
write-toexcelrange -Range $mysheet.range("A1") -data $exceldata|out-null
With the following error:
Unable to cast object of type 'System.String[,]' to type 'System.String'.
At C:\data\rangetest.ps1:38 char:9
+ $wrr.value2=$marr
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], InvalidCastException
+ FullyQualifiedErrorId : System.InvalidCastException
It appears as if the value2 property behaves differently in Windows10 which is weird considering it´s the same version of excel.
Now to the question:
Is there a fix/workaround to getting the data into the cells, which does not involve looping through all the cells.
Update 1
It was suggested by Grade 'Eh' Bacon that I try the .Formula property. It Works! I also noted that Windows10 uses Powershell v5 while my Windows7 has Powershell v4.
Since that worked for you I'll flesh it out as an answer. To summarize, pay attention to the differences between .text, .value, .value2, and .formula [or .formulaR1C1]. See discussion of the first 3 here:
What is the difference between .text, .value, and .value2?
And discussion of .Formula here:
Can Range.Value2 & Range.Formula have different values in C#, not VBA?
Without getting into why any of these can have different values (in short, formatting and other metadata can have an impact on some of those options in different ways, depending on what type of entry is made to a given cell), after reading those Q&As above, I just always use Formula when referring to what's inside a cell. In most cases, that's what you likely want VBA to look at anyway. Changing .value2 to .formula seems to work here, although I have no idea why that would be the case between Windows versions.
I have imported a comma seperated csv file using powershell.
I gets imported and looks as it should. The problem is, the cells contain formulas.
Like =20+50+70. It doesn't get calculated unless i click enter i the top field.
Another problem is, that some of the cells contains numbers like =50,2+70,5. These cells excel doesn't understand at all. It can't caltulate them, unless i remove the , or replace it with a dot (.). But this is not a possibility.
How to i fix this?
The csv file is imported with powershell using this:
[threading.thread]::CurrentThread.CurrentCulture = 'en-US'
$wbpath=Join-Path "$psscriptroot" 'file.xlsx'
$importcsv=Join-Path "$psscriptroot" 'file.csv'
$xl = New-Object -ComObject Excel.Application
$xl.Visible = $false
$xl.Workbooks.OpenText($importcsv)
$xl.DisplayAlerts = $false
[threading.thread]::CurrentThread.CurrentCulture = 'en-US'
$xl.ActiveWorkbook.SaveAs($wbpath,51)
$xl.Quit()
while([System.Runtime.Interopservices.Marshal]::ReleaseComObject($xl)){'released'}
The
[threading.thread]::CurrentThread.CurrentCulture = 'en-US'
is necessary or i will get errors because my system locale is not us.
Thank you.
CSV Sample:
name1.name1.name1,"=20","=7,65","=20,01"
name2.name2.name2,"=20+10","=4,96+0,65","=20,01+10"
name3.name3.name3,"=20","=4,96+0,88","=21,01+11"
Sounds like you need to
a) Force the worksheet to calculate
b) If you're going to stick with en-US locale then you need to replace those commas with decimal points. That's the GB/US standard and how Excel will interpret decimals. I'd strongly advise however that you stick to the locale that your data is set up in.
(untested as I'm currently on a Mac)
[threading.thread]::CurrentThread.CurrentCulture = 'en-US'
$wbpath=Join-Path "$psscriptroot" 'file.xlsx'
$importcsv=Join-Path "$psscriptroot" 'file.csv'
$xl = New-Object -ComObject Excel.Application
$xl.Visible = $false
$wb = $xl.Workbooks.OpenText($importcsv)
$xl.DisplayAlerts = $false
[threading.thread]::CurrentThread.CurrentCulture = 'en-US'
$sh = $wb.Sheets.Item(1)
# loop through the used range and replace any commas with decimals
foreach ($cell in $sh.usedRange)
{
[string]$formula = $cell.formula
$formula -replace ',','.'
$cell.formula = $formula
}
# force the sheet to calculate
$sh.Calculate()
$xl.ActiveWorkbook.SaveAs($wbpath,51)
$xl.Quit()
while([System.Runtime.Interopservices.Marshal]::ReleaseComObject($xl)){'released'}
As with the previous answer, you have to account for locale; not all .csv files are the same formatting based on what country locale they were encoded in. While UTF is standard, in some respects CSV is a "legacy format", even if it's the most lightweight, simple way to transfer data using plaintext.
Sam already answered the majority of the difficult stuff, so I'll just add a few things. If you are making an automated solution and work with multiple countries, there's a few ways you can determine how it's encoded. You can go the more technically proficient route and implement a custom function similar to this one https://gist.github.com/jpoehls/2406504 or, because it's a CSV, you can make a decent guess since the most common encoding formats use different delimiters; I believe the one you are mentioning uses tabs as encoding.
I'll focus on the ones within Excel importing because those weren't mentioned. There's a fairly neat function in the Data tab that allows you to customize your import based on what delimiters it uses. In the third step when you press Advanced, it allows you to tell it which separator (comma or decimal) that the source data is using, and once you select that and press Finish, it will convert the result to whatever your locale is set to for Excel and properly evaluate functions. Example picture So, the workflow for this would be open a new Excel book, select Data > From Text and proceed from there. It will convert the text from the locale you choose (in your case 1252 is likely) into whatever decimal format you specify.
I want to use Powershell to find special characters (like Greek letters) in an Excel document and replace them with HTML entities. My script looks like this:
$file = "C:\Path\To\File\test.xls"
$xl = New-Object -ComObject Excel.Application
$xl.Visible = $True
$objWorkbook = $xl.Workbooks.Open($file)
$objWorksheet = $objWorkbook.Worksheets.Item(1)
$objRange = $objWorksheet.UsedRange
$charArray = #(
([char]948, "δ"),
([char]916, "Δ")
)
foreach ($char in $charArray){
$FindText = $char[0]
$ReplaceText = $char[1]
if ($objRange.find("$FindText")) {
$objRange.replace("$FindText", $ReplaceText)
} else {write-host "Didn't find $FindText"}
}
The trouble is, the .find() and .replace() methods are not case-sensitive, so [char]948 (δ) matches both the lowercase delta (δ) and uppercase delta (Δ) characters. The result is that all δ and Δ characters in the Excel (.xls) file are replaced with δ.
In VBA, Range.Find() has a MatchCase parameter, but it does not seem that Powershell allows it. For example, $objRange.find("$FindText", MatchCase:=$True) does not work.
I also tried Powershell's -cmatch and -creplace commands, which are case-sensitive, but I could not figure out how to get those to work on the Excel range object $objRange:
$objRange -creplace "$FindText", $ReplaceText has no effect on the Excel file.
I can't export or convert the data to .txt or .csv because the special characters don't survive the conversion.
Is there a way to make this work?
Using PowerShell you can use creplace operator
"aAaAaA" -creplace 'a','I'
IAIAIA
To replace find you can use the IndexOf method from the string class it takes a comparisonType
IndexOf(string value, int startIndex, int count, System.StringComparison comparisonType)
Example :
"Jean Paul".indexOF("paul", 0, [System.StringComparison]::CurrentCulture)
-1
"Jean Paul".indexOF("paul", 0, [System.StringComparison]::CurrentCultureIgnoreCase)
5
Why is it that writing cell values to Excel is a lot faster in VBScript than in PowerShell?
Isn't PowerShell the new thing, and VBScript the deprecated MS scripting language?
VBScript example (save to filename.vbs)
This runs in a split second.
Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = false
Set objWorkbook = objExcel.Workbooks.Add()
' Edit: increased number of writes to 500 to make speed difference more noticeable
For row = 1 To 500
'Edit: using .cells(row,1) instead of .cells(50,1) - this was a mistake
objWorkbook.workSheets(1).cells(row,1).value = "test"
Next
objWorkbook.SaveAs(CreateObject("Scripting.FileSystemObject").GetParentFolderName(WScript.ScriptFullName) & "\test.xlsx")
objExcel.Quit
msgbox "Done."
PowerShell example (save to filename.ps1) This takes multiple seconds to run (problematic on thousands of records)
#need this to work around bug if you use a non-US locale: http://support.microsoft.com/default.aspx?scid=kb;en-us;320369
[System.Threading.Thread]::CurrentThread.CurrentCulture = "en-US"
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $False
$xls_workbook = $excel.Workbooks.Add()
# Edit: using foreach instead of for
# Edit: increased number of writes to 500 to make speed difference more noticeable
foreach ($row in 1..500) {
# Edit: Commented out print-line, slows down the script
#"Row " + $row
# This is very slow! - http://forums.redmondmag.com/forums/forum_posts.asp?tid=4037&pn=7
$xls_workbook.sheets.item(1).cells.item($row,1) = "test"
}
$xls_workbook.SaveAs($MyInvocation.MyCommand.Definition.Replace($MyInvocation.MyCommand.Name, "") + "test.xlsx")
$excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)
I want to use this for thousands of records. If there is no fast way to do this, PowerShell is not an option. Are there better alternatives?
You can speed things up by not looping through individual cells:
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $True
$xls_workbook = $excel.Workbooks.Add()
$range = $xls_workbook.sheets.item(1).Range("A1:A100")
$range.Value2 = "test"
If you want to write an array of values to a range, here is a nice blog post that demonstrates similar technique:
How to Get Data into an Excel Spreadsheet Very Quickly with PowerShell
some things don't add up here:
your VBScript, writes on ONE cell over and over, while your PowerShell code writes into 100 cells
objWorkbook.workSheets(1).cells(50,1).value = "test"
$xls_workbook.sheets.item(1).cells.item($row,1) = "test"
you are executing "Row " + $row on PowerShell - this might offset comparison too.
If you want to write into multiple cells, you should think about using arrays and wrinting onto whole ranges, because this has better performance.
You can shave a little time off the PowerShell version by eliminating the for loop test and using a foreach.
for ($row = 1; $row -le 100; $row++)
goes to:
foreach ($row in 1..100)
By doing this you eliminate the comparison and increment.
But aside from that, my observations match yours (see my comments on Jook's answer).
You're still interfacing with Excel through COM though. That's adding some overhead due to COMInterop processing.
PowerShell, by its very design and use of cmdlets is a non-standard mess, at least for basic things. VBScript, which any programmer should be able to use and understand, has a general way of doing basic things that does not require special cmdlets to be installed or included with the deployed code. I believe this is a step backwards in many respects.
Before anyone trashes me and says I just don't PowerShell, I must mention I have a long history of UNIX shell scripting behind me. PowerShell is similar, obviously, but to me its not nearly as well implemented.
I do know that reality dictates that I will end up using PowerShell sooner or later - I just hope it evolves into a more "standard" replacement in the future.