fix: aggregate Plant3D plugin usage without computer field
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
param(
|
||||
[string]$SourceRoot = 'C:\dev\TEMP\CheckPlugin',
|
||||
[string]$SheetName = 'Plant3D',
|
||||
[string]$OutputDirectory = 'C:\dev\autodesk\migration\audits\plugin-usage-plant3d-aggregate-20260731'
|
||||
)
|
||||
$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$mainNs='http://schemas.openxmlformats.org/spreadsheetml/2006/main';$relNs='http://schemas.openxmlformats.org/officeDocument/2006/relationships';$pkgNs='http://schemas.openxmlformats.org/package/2006/relationships';$utf8=New-Object Text.UTF8Encoding($false);$utf8Bom=New-Object Text.UTF8Encoding($true)
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $OutputDirectory 'evidence')|Out-Null
|
||||
function XmlEntry($z,$name){$e=$z.GetEntry($name.TrimStart('/'));if(!$e){return $null};$r=[IO.StreamReader]::new($e.Open());try{return [xml]$r.ReadToEnd()}finally{$r.Dispose()}}
|
||||
function Col($ref){$s=([regex]::Match($ref,'^[A-Z]+')).Value;$n=0;foreach($x in $s.ToCharArray()){$n=$n*26+([int][char]$x-64)};$n}
|
||||
function HNorm($v){if($null -eq $v){return ''};(($v.Trim().ToLowerInvariant())-replace '[\s_\-\.]+','')}
|
||||
function PNorm($v){if($null -eq $v){return ''};(($v.Trim()-replace '(?i)\.(dll|exe)$','').Trim().ToUpperInvariant())}
|
||||
function Shared($z){$x=XmlEntry $z 'xl/sharedStrings.xml';$a=[Collections.Generic.List[string]]::new();if(!$x){return $a};$n=New-Object Xml.XmlNamespaceManager($x.NameTable);$n.AddNamespace('m',$mainNs);foreach($i in $x.SelectNodes('//m:si',$n)){$a.Add((($i.SelectNodes('.//m:t',$n)|% InnerText)-join ''))};$a}
|
||||
function CellText($c,$n,$ss){if(!$c){return ''};$i=$c.SelectSingleNode('./m:is',$n);if($i){return (($i.SelectNodes('.//m:t',$n)|% InnerText)-join '')};$v=$c.SelectSingleNode('./m:v',$n);if(!$v){return ''};$s=$v.InnerText;if([string]$c.t -eq 's'){$j=0;if([int]::TryParse($s,[ref]$j)-and $j-lt $ss.Count){return $ss[$j]}};$s}
|
||||
function DateStyles($z){$h=[Collections.Generic.HashSet[int]]::new();14..22|%{[void]$h.Add($_)};$x=XmlEntry $z 'xl/styles.xml';if(!$x){return $h};$n=New-Object Xml.XmlNamespaceManager($x.NameTable);$n.AddNamespace('m',$mainNs);$custom=@{};foreach($f in $x.SelectNodes('//m:numFmts/m:numFmt',$n)){$custom[[int]$f.numFmtId]=[string]$f.formatCode};$i=0;foreach($xf in $x.SelectNodes('//m:cellXfs/m:xf',$n)){$id=[int]$xf.numFmtId;if(($custom.ContainsKey($id)-and $custom[$id]-match 'yy|yyyy|mm|dd|hh|ss')-or($id-ge 14-and $id-le 22)){[void]$h.Add($i)};$i++};$h}
|
||||
function DateValue($text,$numeric,$style,$styles){if([string]::IsNullOrWhiteSpace($text)){return $null};if($numeric-and $styles.Contains($style)){$d=0.0;if([double]::TryParse($text,[Globalization.NumberStyles]::Float,[Globalization.CultureInfo]::InvariantCulture,[ref]$d)){return [pscustomobject]@{Date=[DateTime]::FromOADate($d);Rule='EXCEL_SERIAL_DATE'}}};$v=($text.Trim()-replace '[Tt]',' '-replace '\s+',' ');$fmts='dd.MM.yyyy HH:mm:ss','dd.MM.yyyy HH:mm','dd.MM.yyyy','yyyy-MM-dd HH:mm:ss','yyyy-MM-dd','yyyy MM dd HH:mm:ss','yyyy MM dd','dd/MM/yyyy HH:mm:ss','dd/MM/yyyy';foreach($f in $fmts){$d=[DateTime]::MinValue;if([DateTime]::TryParseExact($v,$f,[Globalization.CultureInfo]::InvariantCulture,[Globalization.DateTimeStyles]::AllowWhiteSpaces,[ref]$d)){return [pscustomobject]@{Date=$d;Rule=if($f-match 'H|s'){'TEXT_DATETIME'}else{'DATE_ONLY'}}}};$d=[DateTime]::MinValue;if([DateTime]::TryParse($v,[Globalization.CultureInfo]::GetCultureInfo('ru-RU'),[Globalization.DateTimeStyles]::AllowWhiteSpaces,[ref]$d)){return [pscustomobject]@{Date=$d;Rule=if($v-match ':'){'TEXT_DATETIME'}else{'DATE_ONLY'}}};$null}
|
||||
function Sheet($z,$wanted){$w=XmlEntry $z 'xl/workbook.xml';$r=XmlEntry $z 'xl/_rels/workbook.xml.rels';$n=New-Object Xml.XmlNamespaceManager($w.NameTable);$n.AddNamespace('m',$mainNs);$n.AddNamespace('r',$relNs);$rn=New-Object Xml.XmlNamespaceManager($r.NameTable);$rn.AddNamespace('p',$pkgNs);foreach($s in $w.SelectNodes('//m:sheet',$n)){if(([string]$s.name).Trim().ToLowerInvariant() -eq $wanted.Trim().ToLowerInvariant()){$id=$s.GetAttribute('id',$relNs);$rel=$r.SelectSingleNode("//p:Relationship[@Id='$id']",$rn);$t=([string]$rel.Target).TrimStart('/');if($t-notmatch '^xl/'){$t='xl/'+$t};return [pscustomobject]@{Name=[string]$s.name;Target=$t}}};$null}
|
||||
function Rows($z,$target,$ss,$styles){$x=XmlEntry $z $target;$n=New-Object Xml.XmlNamespaceManager($x.NameTable);$n.AddNamespace('m',$mainNs);$out=[Collections.Generic.List[object]]::new();foreach($row in $x.SelectNodes('//m:sheetData/m:row',$n)){$cells=@{};foreach($c in $row.SelectNodes('./m:c',$n)){$col=Col ([string]$c.r);$text=CellText $c $n $ss;$num=([string]$c.t -eq ''-or[string]$c.t-eq'n');$st=if([string]$c.s){[int]$c.s}else{0};$cells[$col]=[pscustomobject]@{Text=$text;Date=DateValue $text $num $st $styles;Formula=[string]$c.f}};$out.Add([pscustomobject]@{Number=[int]$row.r;Cells=$cells})};$out}
|
||||
function C($row,$col){if($row.Cells.ContainsKey($col)){return $row.Cells[$col].Text};''}
|
||||
function Header($rows){$pa=@('плагин','plugin','pluginname','module','command');$da=@('дата','date','datetime','timestamp','launchdate','usage_date');$la=@('количествозапусков','launchcount','count','usagecount','runs');foreach($row in $rows|select -First 30){$m=@{};foreach($k in $row.Cells.Keys){$m[(HNorm (C $row $k))]=[int]$k};$p=$pa|?{$m.ContainsKey($_)}|select -First 1;$d=$da|?{$m.ContainsKey($_)}|select -First 1;$l=$la|?{$m.ContainsKey($_)}|select -First 1;if($p-and$d-and$l){return [pscustomobject]@{Row=$row;Plugin=$m[$p];Date=$m[$d];Count=$m[$l];Rule='HEADER_ALIASES'}}};$null}
|
||||
function Hash($p){(Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash}
|
||||
function RowHash($id,$row,$p,$d,$c){$s=[Security.Cryptography.SHA256]::Create();try{([BitConverter]::ToString($s.ComputeHash([Text.Encoding]::UTF8.GetBytes("$id|$row|$p|$d|$c")))-replace '-','')}finally{$s.Dispose()}}
|
||||
function Csv($rows,$path,$headers){if(@($rows).Count){$rows|Export-Csv -LiteralPath $path -NoTypeInformation -Encoding UTF8}else{[IO.File]::WriteAllText($path,($headers-join ',')+"`r`n",$utf8Bom)}}
|
||||
|
||||
$files=@(Get-ChildItem -LiteralPath $SourceRoot -Recurse -File -Filter '*.xlsx'|? Name -notlike '~$*'|Sort FullName);$ignore=@(Get-ChildItem -LiteralPath $SourceRoot -Recurse -File -Filter '~$*.xlsx' -ErrorAction SilentlyContinue).Count;$fileRows=[Collections.Generic.List[object]]::new();$dups=[Collections.Generic.List[object]]::new();$reject=[Collections.Generic.List[object]]::new();$raw=[Collections.Generic.List[object]]::new();$idx=0;$rawRows=0;$validPlugin=0;$validDate=0;$validCount=0;$computerPropertyHits=0
|
||||
$propertyNames='ComputerName','MachineName','HostName';foreach($f in $files){$z=[IO.Compression.ZipFile]::OpenRead($f.FullName);try{foreach($part in 'docProps/core.xml','docProps/custom.xml','docProps/app.xml'){$e=$z.GetEntry($part);if($e){$r=[IO.StreamReader]::new($e.Open());$text=$r.ReadToEnd();$r.Dispose();foreach($name in $propertyNames){if($text -match ('(?i)'+$name)){$computerPropertyHits++}}}}}finally{$z.Dispose()}}
|
||||
$byHash=@{};foreach($f in $files){$idx++;$id='source-{0:D4}.xlsx'-f $idx;$h=Hash $f.FullName;if(!$byHash.ContainsKey($h)){$byHash[$h]=[pscustomobject]@{File=$f;Id=$id;Hash=$h;Canonical=$true}}else{$dups.Add([pscustomobject]@{SourceId=$id;CanonicalSourceId=$byHash[$h].Id;SHA256=$h;Status='DUPLICATE_WORKBOOK'})}}
|
||||
$idx=0;foreach($f in $files){$idx++;$id='source-{0:D4}.xlsx'-f $idx;$h=Hash $f.FullName;$canonical=($byHash[$h].Id -eq $id);$before=Get-Item -LiteralPath $f.FullName;$plant=$false;$sheetActual='';$rowCount=0;$colCount=0;$result='DUPLICATE_WORKBOOK';$rejBefore=$reject.Count;$headerText='';if(!$canonical){$fileRows.Add([pscustomobject]@{SourceId=$id;SHA256=$h;HasPlant3DSheet='UNKNOWN_DUPLICATE';ActualSheetName='';RowCount=0;ColumnCount=0;Headers='';ParseResult='DUPLICATE_WORKBOOK';RejectedRows=0});continue};try{$z=[IO.Compression.ZipFile]::OpenRead($f.FullName);try{$sh=Sheet $z $SheetName;if(!$sh){$reject.Add([pscustomobject]@{SourceId=$id;SourceSheet='';SourceRow='';ReasonCode='SHEET_NOT_FOUND';DetectedPlugin='';DetectedDate='';DetectedLaunchCount='';RowHash=(RowHash $id 0 '' '' '')});$result='SHEET_NOT_FOUND';$fileRows.Add([pscustomobject]@{SourceId=$id;SHA256=$h;HasPlant3DSheet=$false;ActualSheetName='';RowCount=0;ColumnCount=0;Headers='';ParseResult=$result;RejectedRows=1});continue};$plant=$true;$sheetActual=$sh.Name;$ss=Shared $z;$styles=DateStyles $z;$rr=Rows $z $sh.Target $ss $styles;$rowCount=$rr.Count;$colCount=if($rr.Count){($rr|%{if($_.Cells.Count){($_.Cells.Keys|measure -Maximum).Maximum}else{0}}|measure -Maximum).Maximum}else{0};$hd=Header $rr;if(!$hd){$reject.Add([pscustomobject]@{SourceId=$id;SourceSheet=$sheetActual;SourceRow='';ReasonCode='HEADER_NOT_FOUND';DetectedPlugin='';DetectedDate='';DetectedLaunchCount='';RowHash=(RowHash $id 0 '' '' '')});$result='HEADER_NOT_FOUND';continue};$headerText=(($hd.Row.Cells.Keys|Sort|%{C $hd.Row $_})-join ';');foreach($row in $rr|? Number -gt $hd.Row.Number){$plugin=(C $row $hd.Plugin).Trim();$dateText=(C $row $hd.Date).Trim();$countText=(C $row $hd.Count).Trim();if(!$plugin-and !$dateText-and !$countText){continue};$rawRows++;$pNorm=PNorm $plugin;$di=$row.Cells[$hd.Date].Date;$num=0.0;$countOk=[double]::TryParse(($countText-replace ',','.'),[Globalization.NumberStyles]::Float,[Globalization.CultureInfo]::InvariantCulture,[ref]$num);$reason='';if(!$plugin){$reason='PLUGIN_NOT_FOUND'}else{$validPlugin++};if(!$dateText){$reason=if($reason){'AMBIGUOUS_RECORD'}else{'DATE_NOT_FOUND'}}elseif(!$di){$reason=if($reason){'AMBIGUOUS_RECORD'}else{'INVALID_DATE'}}else{$validDate++};if(!$countText){$reason=if($reason){'AMBIGUOUS_RECORD'}else{'INVALID_LAUNCH_COUNT'}}elseif(!$countOk-or [double]::IsNaN($num)-or [double]::IsInfinity($num)-or $num-lt 0-or $num-ne [math]::Truncate($num)){$reason=if($reason){'AMBIGUOUS_RECORD'}else{'INVALID_LAUNCH_COUNT'}}else{$validCount++};if($reason){$reject.Add([pscustomobject]@{SourceId=$id;SourceSheet=$sheetActual;SourceRow=$row.Number;ReasonCode=$reason;DetectedPlugin=$plugin;DetectedDateTime=$dateText;DetectedLaunchCount=$countText;RowHash=(RowHash $id $row.Number $plugin $dateText $countText)});continue};$d=$di.Date;$raw.Add([pscustomobject]@{PluginName=$plugin;PluginNameNormalized=$pNorm;UsageDate=$d.ToString('yyyy-MM-dd');UsageMonth=$d.ToString('yyyy-MM');UsageYear=$d.ToString('yyyy');LaunchCount=[int64]$num;SourceId=$id;SourceRow=$row.Number;ParseRule=$di.Rule})}}finally{$z.Dispose()};$result='PARSED'}catch{$reject.Add([pscustomobject]@{SourceId=$id;SourceSheet=$sheetActual;SourceRow='';ReasonCode='READ_ERROR';DetectedPlugin='';DetectedDateTime='';DetectedLaunchCount='';RowHash=(RowHash $id 0 '' '' '')});$result='READ_ERROR'}$after=Get-Item -LiteralPath $f.FullName;if($before.Length-ne $after.Length-or $before.LastWriteTimeUtc-ne $after.LastWriteTimeUtc-or(Hash $f.FullName)-ne $h){throw "Source modified: $id"};$fileRows.Add([pscustomobject]@{SourceId=$id;SHA256=$h;HasPlant3DSheet=$plant;ActualSheetName=$sheetActual;RowCount=$rowCount;ColumnCount=$colCount;Headers=$headerText;ParseResult=$result;RejectedRows=($reject.Count-$rejBefore)})}
|
||||
|
||||
$dupCandidates=@($raw|Group-Object PluginNameNormalized,UsageDate,LaunchCount|? Count -gt 1);$plugin=[Collections.Generic.List[object]]::new();foreach($g in ($raw|Group PluginNameNormalized)){$a=@($g.Group);$byDay=@($a|Group UsageDate);$byMonth=@($a|Group UsageMonth);$month=$byMonth|Sort Count -Descending|Select -First 1;$plugin.Add([pscustomobject]@{PluginName=$a[0].PluginName;PluginNameNormalized=$g.Name;RecordCount=$a.Count;TotalLaunches=(($a.LaunchCount|Measure -Sum).Sum);FirstUsage=($a.UsageDate|Sort|Select -First 1);LastUsage=($a.UsageDate|Sort|Select -Last 1);ActiveDays=($a.UsageDate|Sort -Unique).Count;ActiveMonths=($a.UsageMonth|Sort -Unique).Count;AverageLaunchesPerActiveDay=[math]::Round((($a.LaunchCount|Measure -Sum).Sum/($a.UsageDate|Sort -Unique).Count),4);MaxLaunchesPerDay=(($byDay|%{($_.Group.LaunchCount|Measure -Sum).Sum}|Measure -Maximum).Maximum);MaxUsageMonth=$month.Name;MaxMonthLaunches=(($month.Group.LaunchCount|Measure -Sum).Sum);ShareOfAllLaunches=0})};$total=(($raw.LaunchCount|Measure -Sum).Sum);$plugin|%{$_.ShareOfAllLaunches=if($total){[math]::Round($_.TotalLaunches/$total,6)}else{0}};$plugin=@($plugin|Sort PluginName|Sort TotalLaunches -Descending)
|
||||
$day=@($raw|Group PluginNameNormalized,UsageDate|%{$a=$_.Group;[pscustomobject]@{PluginName=$a[0].PluginName;UsageDate=$a[0].UsageDate;LaunchCount=(($a.LaunchCount|Measure -Sum).Sum);SourceRecordCount=$a.Count}}|Sort PluginName,UsageDate);$months= @($raw.UsageMonth|Sort -Unique);$monthRows=@();foreach($g in ($raw|Group PluginNameNormalized)){ $r=[ordered]@{Плагин=$g.Group[0].PluginName};foreach($m in $months){$r[$m]= (($g.Group|? UsageMonth -eq $m|% LaunchCount|Measure -Sum).Sum)};$monthRows+=[pscustomobject]$r }
|
||||
$normalized=@();$n=0;foreach($x in $raw|Sort PluginNameNormalized,UsageDate,SourceId,SourceRow){$n++;$normalized+=[pscustomobject]@{RecordId=('REC-{0:D6}'-f $n);PluginName=$x.PluginName;PluginNameNormalized=$x.PluginNameNormalized;UsageDate=$x.UsageDate;UsageMonth=$x.UsageMonth;LaunchCount=$x.LaunchCount;SourceId=$x.SourceId;SourceRow=$x.SourceRow;ParseRule=$x.ParseRule}}
|
||||
$quality=[pscustomobject]@{XlsxFound=$files.Count;TemporaryIgnored=$ignore;Plant3DSheets=($files.Count-($fileRows|? ParseResult -eq 'SHEET_NOT_FOUND').Count);CanonicalPlant3DSheets=($fileRows|? HasPlant3DSheet -eq $true).Count;WithoutPlant3D=($fileRows|? ParseResult -eq 'SHEET_NOT_FOUND').Count;UniqueWorkbooks=$byHash.Count;DuplicateWorkbooks=$dups.Count;RowsExamined=$rawRows;RowsWithValidPlugin=$validPlugin;RowsWithValidDate=$validDate;RowsWithValidLaunchCount=$validCount;ValidAggregateRows=$raw.Count;RejectedRows=$reject.Count;TotalLaunches=$total;UniquePlugins=($raw.PluginNameNormalized|Sort -Unique).Count;DateRange=if($raw.Count){"$(($raw.UsageDate|Sort|Select -First 1)) .. $(($raw.UsageDate|Sort|Select -Last 1))"}else{''};SourceModified='NO';ComputerFieldAvailable=$false;ExplicitComputerPropertyHits=$computerPropertyHits}
|
||||
Csv $plugin (Join-Path $OutputDirectory 'plugin-summary.csv') 'PluginName,PluginNameNormalized,RecordCount,TotalLaunches,FirstUsage,LastUsage,ActiveDays,ActiveMonths,AverageLaunchesPerActiveDay,MaxLaunchesPerDay,MaxUsageMonth,MaxMonthLaunches,ShareOfAllLaunches';Csv $day (Join-Path $OutputDirectory 'plugin-day-matrix.csv') 'PluginName,UsageDate,LaunchCount,SourceRecordCount';Csv $monthRows (Join-Path $OutputDirectory 'plugin-month-matrix.csv') 'Плагин';Csv $normalized (Join-Path $OutputDirectory 'normalized-usage-records.csv') 'RecordId,PluginName,PluginNameNormalized,UsageDate,UsageMonth,LaunchCount,SourceId,SourceRow,ParseRule';Csv $dups (Join-Path $OutputDirectory 'duplicate-workbooks.csv') 'SourceId,CanonicalSourceId,SHA256,Status';Csv $reject (Join-Path $OutputDirectory 'rejected-records.csv') 'SourceId,SourceSheet,SourceRow,ReasonCode,DetectedPlugin,DetectedDate,DetectedLaunchCount,RowHash';Csv $fileRows (Join-Path $OutputDirectory 'source-inventory.csv') 'SourceId,SHA256,HasPlant3DSheet,ActualSheetName,RowCount,ColumnCount,Headers,ParseResult,RejectedRows';[IO.File]::WriteAllText((Join-Path $OutputDirectory 'summary.json'),($quality|ConvertTo-Json -Depth 3),$utf8)
|
||||
[IO.File]::WriteAllText((Join-Path $OutputDirectory 'computer-data-availability.md'),("# Computer data availability`r`n`r`nThe source workbooks do not contain an explicit computer-name column. Explicit ComputerName/MachineName/HostName properties in core, custom and app properties: "+$computerPropertyHits+". The User field is not a reliable substitute and was not used. A plugin x computer matrix cannot be built from this XLSX set without an additional source: MachineName log, computer name in workbook name, explicit workbook property, User-to-computer mapping, or the source database."),$utf8)
|
||||
function E($v){if($null -eq $v){return ''};[Security.SecurityElement]::Escape([string]$v)};function XCol($n){$s='';while($n-gt 0){$n--; $s=([char](65+($n%26)))+$s;$n=[math]::Floor($n/26)};$s};function SX($rows){$b=[Text.StringBuilder]::new();[void]$b.Append('<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>');$r=1;foreach($row in $rows){[void]$b.Append('<row r="'+$r+'">');$c=1;foreach($p in $row.PSObject.Properties){[void]$b.Append('<c r="'+(XCol $c)+$r+'" t="inlineStr"><is><t xml:space="preserve">'+(E $p.Value)+'</t></is></c>');$c++};[void]$b.Append('</row>');$r++};[void]$b.Append('</sheetData></worksheet>');$b.ToString()}
|
||||
function XLSX($path,$sheets){$fs=[IO.File]::Create($path);$z=[IO.Compression.ZipArchive]::new($fs,[IO.Compression.ZipArchiveMode]::Create);try{$ct='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';for($i=1;$i-le $sheets.Count;$i++){$ct+='<Override PartName="/xl/worksheets/sheet'+$i+'.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'};$ct+='</Types>';$root='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>';$wb='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>';$rels='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';$i=1;foreach($name in $sheets.Keys){$wb+='<sheet name="'+(E $name)+'" sheetId="'+$i+'" r:id="rId'+$i+'"/>';$rels+='<Relationship Id="rId'+$i+'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet'+$i+'.xml"/>';$i++};$wb+='</sheets></workbook>';$rels+='</Relationships>';$entries=[ordered]@{'[Content_Types].xml'=$ct;'_rels/.rels'=$root;'xl/workbook.xml'=$wb;'xl/_rels/workbook.xml.rels'=$rels};$i=1;foreach($name in $sheets.Keys){$entries['xl/worksheets/sheet'+$i+'.xml']=SX $sheets[$name];$i++};foreach($e in $entries.GetEnumerator()){$x=$z.CreateEntry($e.Key);$x.LastWriteTime=[DateTimeOffset]::new(2000,1,1,0,0,0,[TimeSpan]::Zero);$w=[IO.StreamWriter]::new($x.Open(),$utf8);try{$w.Write($e.Value)}finally{$w.Dispose()}}}finally{$z.Dispose();$fs.Dispose()}}
|
||||
$sumRows=@([pscustomobject]@{Плагин='Плагин';'Количество записей'='Количество записей';'Количество запусков'='Количество запусков';'Первое использование'='Первое использование';'Последнее использование'='Последнее использование';'Активных дней'='Активных дней';'Активных месяцев'='Активных месяцев';'Среднее запусков на активный день'='Среднее запусков на активный день';'Максимум запусков за день'='Максимум запусков за день';'Месяц максимального использования'='Месяц максимального использования';'Запусков в максимальный месяц'='Запусков в максимальный месяц';'Доля от всех запусков'='Доля от всех запусков'})+@($plugin|%{[pscustomobject]@{Плагин=$_.PluginName;'Количество записей'=$_.RecordCount;'Количество запусков'=$_.TotalLaunches;'Первое использование'=$_.FirstUsage;'Последнее использование'=$_.LastUsage;'Активных дней'=$_.ActiveDays;'Активных месяцев'=$_.ActiveMonths;'Среднее запусков на активный день'=$_.AverageLaunchesPerActiveDay;'Максимум запусков за день'=$_.MaxLaunchesPerDay;'Месяц максимального использования'=$_.MaxUsageMonth;'Запусков в максимальный месяц'=$_.MaxMonthLaunches;'Доля от всех запусков'=$_.ShareOfAllLaunches}});$dayRows=@([pscustomobject]@{Плагин='Плагин';Дата='Дата';'Количество запусков'='Количество запусков';'Количество исходных строк'='Количество исходных строк'})+@($day|%{[pscustomobject]@{Плагин=$_.PluginName;Дата=$_.UsageDate;'Количество запусков'=$_.LaunchCount;'Количество исходных строк'=$_.SourceRecordCount}});$sourceRows=@([pscustomobject]@{Плагин='Плагин';Дата='Дата';'Количество запусков'='Количество запусков';'Анонимизированный источник'='Анонимизированный источник';'Исходная строка'='Исходная строка'})+@($normalized|%{[pscustomobject]@{Плагин=$_.PluginName;Дата=$_.UsageDate;'Количество запусков'=$_.LaunchCount;'Анонимизированный источник'=$_.SourceId;'Исходная строка'=$_.SourceRow}});$qualityRows=@($quality.PSObject|%{[pscustomobject]@{Показатель=$_.Name;Значение=$_.Value}});$sheets=[ordered]@{'Итог по плагинам'=$sumRows;'Плагин × месяц'=@([pscustomobject]@{Плагин='Плагин'})+@($monthRows);'Плагин × день'=$dayRows;'Исходные агрегаты'=$sourceRows;'Качество данных'=$qualityRows};XLSX (Join-Path $OutputDirectory 'plugin-usage-final.xlsx') $sheets
|
||||
$readme="XLSX files: $($quality.XlsxFound)`r`nUnique workbooks by SHA-256: $($quality.UniqueWorkbooks)`r`nDuplicate workbooks: $($quality.DuplicateWorkbooks)`r`nRows examined: $($quality.RowsExamined)`r`nValid aggregate rows: $($quality.ValidAggregateRows)`r`nRejected rows: $($quality.RejectedRows)`r`nUnique plugins: $($quality.UniquePlugins)`r`nDate range: $($quality.DateRange)`r`nTotal launches: $($quality.TotalLaunches)`r`nMost used plugin: $($plugin|select -First 1 -Expand PluginName)`r`nMost active month: $((($raw|group UsageMonth)|sort Count -Descending|select -First 1).Name)`r`nComputer field available: NO`r`nComputer matrix generated: NO`r`n`r`nOnly Plant3D sheets were analyzed. Source files are anonymized as source-0001.xlsx; project numbers, names, users and paths are not published.`r`n`r`nThe semantic of File was not used for deduplication: rows with same plugin/date/count were retained because File may represent separate aggregate dimensions.`r`n`r`nRepeat: .\tools\plugin-usage\Build-Plant3DPluginUsageAggregate.ps1 -SourceRoot C:\dev\TEMP\CheckPlugin -SheetName Plant3D -OutputDirectory .\migration\audits\plugin-usage-plant3d-aggregate-20260731";[IO.File]::WriteAllText((Join-Path $OutputDirectory 'README.md'),$readme,$utf8)
|
||||
Reference in New Issue
Block a user