# Escribe fórmulas en Excel de Microsoft 365, cada una en un libro limpio, y anota lo que da cada una. # Lo llama excel.mjs; no hace falta correrlo a mano. # # Uso: pwsh -NoProfile -File evaluar.ps1 -Trabajo trabajo.json -Salida salida.jsonl # # trabajo.json: { "libros": { "prueba": { "Ventas": { "encabezados": [...], "fechas": [0], "filas": [[...]] }, ... } }, # "evaluaciones": [ { "id": "...", "variante": "prueba", "hoja": "Ventas", "celda": "K2", # "formula": "=...", "forma": "local" | "ingles" } ] } # Las fechas llegan como número de serie de Excel. Cada evaluación sale como una línea JSON en salida.jsonl, en # cuanto termina, para que excel.mjs sepa en cuál se quedó si Excel se cuelga. # # Una fórmula se escribe como la pega una persona en Microsoft 365, con matrices dinámicas: Formula2Local para la # forma local (en esta máquina, nombres en español y punto y coma) y Formula2 para la forma inglesa de Excel. # Excel se abre en una instancia propia y oculta, sin tocar las opciones guardadas del usuario, y se cierra al acabar. param( [Parameter(Mandatory)] [string] $Trabajo, [Parameter(Mandatory)] [string] $Salida ) $ErrorActionPreference = 'Stop' Add-Type -Namespace Shinacer -Name Ventana -MemberDefinition @' [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); '@ # Los códigos de error de Excel (xlErr…) en el valor de una celda $errores = @{ 2000 = '#NULL!'; 2007 = '#DIV/0!'; 2015 = '#VALUE!'; 2023 = '#REF!'; 2029 = '#NAME?'; 2036 = '#NUM!'; 2042 = '#N/A' 2043 = '#GETTING_DATA'; 2045 = '#SPILL!'; 2046 = '#CONNECT!'; 2047 = '#BLOCKED!'; 2048 = '#UNKNOWN!'; 2049 = '#FIELD!' 2050 = '#CALC!' } $encargo = Get-Content -Raw -Encoding utf8 $Trabajo | ConvertFrom-Json -AsHashtable # Cada hoja, como matrices listas para pegar de una vez (encabezados y datos) function ConvertirA-Matriz($filas, [int] $columnas) { $m = New-Object 'object[,]' $filas.Count, $columnas for ($i = 0; $i -lt $filas.Count; $i++) { for ($j = 0; $j -lt $filas[$i].Count; $j++) { $v = $filas[$i][$j] if ($null -eq $v) { continue } $m[$i, $j] = if ($v -is [string]) { $v } else { [double] $v } } } return , $m } $libros = @{} foreach ($variante in $encargo.libros.Keys) { $libros[$variante] = @{} foreach ($nombre in $encargo.libros[$variante].Keys) { $hoja = $encargo.libros[$variante][$nombre] $columnas = $hoja.encabezados.Count $libros[$variante][$nombre] = @{ columnas = $columnas encabezados = ConvertirA-Matriz @(, $hoja.encabezados) $columnas datos = ConvertirA-Matriz $hoja.filas $columnas filas = $hoja.filas.Count fechas = $hoja.fechas } } } function Escribir-Linea($objeto) { $linea = $objeto | ConvertTo-Json -Compress -Depth 5 [IO.File]::AppendAllText($Salida, $linea + "`n", [Text.UTF8Encoding]::new($false)) } $excel = $null $proceso = 0 try { $excel = New-Object -ComObject Excel.Application [uint32] $pid32 = 0 [void][Shinacer.Ventana]::GetWindowThreadProcessId([IntPtr] $excel.Hwnd, [ref] $pid32) $proceso = [int] $pid32 [IO.File]::WriteAllText("$Salida.pid", "$proceso") # Solo opciones de esta sesión: no se guardan en el perfil del usuario $excel.Visible = $false $excel.DisplayAlerts = $false $excel.ScreenUpdating = $false $excel.EnableEvents = $false # xlDayCode, xlMonthCode y xlYearCode $formatoFecha = ($excel.International(21) * 2) + '/' + ($excel.International(20) * 2) + '/' + ($excel.International(19) * 4) Escribir-Linea ([ordered]@{ excel = [ordered]@{ version = $excel.Version; compilacion = $excel.Build; idioma = $excel.LanguageSettings.LanguageID(2) separadorDecimal = $excel.International(3); separadorDeLista = $excel.International(5); pid = $proceso } }) foreach ($ev in $encargo.evaluaciones) { $inicio = [Diagnostics.Stopwatch]::StartNew() $r = [ordered]@{ id = $ev.id; variante = $ev.variante; forma = $ev.forma; aceptada = $false } $libro = $excel.Workbooks.Add(-4167) # xlWBATWorksheet: un libro con una sola hoja try { $hoja = $libro.Worksheets.Item(1) $hoja.Name = $ev.hoja $datos = $libros[$ev.variante][$ev.hoja] $hoja.Range('A1').Resize(1, $datos.columnas).Value2 = $datos.encabezados $hoja.Range('A2').Resize($datos.filas, $datos.columnas).Value2 = $datos.datos # Las fechas se ven como fechas, con las letras de día, mes y año de este Excel (en español, «dd/mm/aaaa»); por # COM, NumberFormat también se lee en la forma local. Solo cambia cómo se ven: la calificación lee el valor foreach ($c in $datos.fechas) { $hoja.Columns.Item($c + 1).NumberFormatLocal = $formatoFecha } $celda = $hoja.Range($ev.celda) try { if ($ev.forma -eq 'ingles') { $celda.Formula2 = $ev.formula } else { $celda.Formula2Local = $ev.formula } $r.aceptada = $true } catch { $r.error = $_.Exception.Message } if ($r.aceptada) { $excel.Calculate() $v = $celda.Value2 $r.valor = if ($null -eq $v) { [ordered]@{ tipo = 'vacio' } } elseif ($v -is [int]) { $codigo = $v + 2146828288 # VT_ERROR: 0x800A0000 + el código de Excel [ordered]@{ tipo = 'error'; codigo = $codigo; v = $errores[$codigo] } } elseif ($v -is [double]) { [ordered]@{ tipo = 'numero'; v = $v } } elseif ($v -is [bool]) { [ordered]@{ tipo = 'logico'; v = $v } } else { [ordered]@{ tipo = 'texto'; v = [string] $v } } $r.derrama = [bool] $celda.HasSpill if ($r.derrama) { $r.celdasDerramadas = $celda.SpillingToRange.Count } $r.texto = [string] $celda.Text $r.formula2 = [string] $celda.Formula2 $r.formula2Local = [string] $celda.Formula2Local } } finally { $libro.Close($false) [void][Runtime.InteropServices.Marshal]::ReleaseComObject($libro) } $r.ms = $inicio.ElapsedMilliseconds Escribir-Linea $r } } finally { if ($excel) { try { $excel.Quit() } catch {} [void][Runtime.InteropServices.Marshal]::ReleaseComObject($excel) } [GC]::Collect() [GC]::WaitForPendingFinalizers() # Si Excel no se cerró con Quit, se cierra su proceso: solo el que abrió este script if ($proceso) { Start-Sleep -Milliseconds 500 $p = Get-Process -Id $proceso -ErrorAction SilentlyContinue if ($p -and $p.ProcessName -eq 'EXCEL') { Stop-Process -Id $proceso -Force } } }