Мне удалось заставить ваш предложенный код работать, но он требует некоторых настроек кода и данных:
- Должен быть какой-то дополнительный шаг, когда вы десериализуете столбец
students
rooms.csv
в коллекцию объектов. Похоже, это ScriptBlock
, который оценивается в массив HashTable
с, но некоторые изменения в CSV-входе все еще необходимы:
- Свойства
StartDate
и EndDate
необходимо заключать в кавычки и приводить к [DateTime]
.
- По крайней мере для комнат, в которых есть несколько учеников, значение должно быть заключено в кавычки, чтобы
Import-Csv
не интерпретировал ,
, разделяющий элементы массива, как дополнительный столбец.
- Недостатком использования CSV в качестве промежуточного формата является потеря оригинальных типов свойств; все становится
[String]
при импорте. Иногда желательно привести к исходному типу в целях эффективности, а иногда это абсолютно необходимо для того, чтобы определенные операции работали. Вы можете разыграть эти свойства каждый раз, когда используете их, но я предпочитаю разыгрывать их один раз сразу после импорта.
С этими изменениями rooms.csv
становится ...
roomName, roomNo, students
room1, 1, "{@{StudentNo=111; StudentName='john smith'; StartDate=[DateTime] '2018-01-01T00:00:00'; EndDate=[DateTime] '2018-07-06T00:00:00'}}"
room2, 2, "{@{StudentNo=222; StudentName='jane doe'; StartDate=[DateTime] '2018-01-01T00:00:00'; EndDate=[DateTime] '2018-07-06T00:00:00'}}"
... и сценарий становится ...
# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
| Select-Object `
-ExcludeProperty 'students' `
-Property '*', @{
Name = 'Students'
Expression = {
$studentsText = $_.students
$studentsScriptBlock = Invoke-Expression -Command $studentsText
$studentsArray = @(& $studentsScriptBlock)
return $studentsArray
}
}
# Replace the [String] property "StudentNo" with an [Int32] property of the same name
$students = Import-Csv students.csv `
| Select-Object `
-ExcludeProperty 'StudentNo' `
-Property '*', @{
Name = 'StudentNo'
Expression = { [Int32] $_.StudentNo }
}
$combined = $students `
| Select-Object -Property `
'StudentNo', `
'PreferredSurname', `
'PreferredFirstnames', `
'UPN', `
@{
Name = "roomName";
Expression = {
foreach ($r in $rooms)
{
if ($r.Students.StudentNo -contains $_.StudentNo)
{
return $r.roomName
}
}
#TODO: Return text indicating room not found?
}
}
Причина, по которой это может быть медленным, заключается в том, что вы выполняете линейный поиск - фактически два из них - для каждого объекта учащегося; сначала через набор комнат (foreach
), затем через набор студентов в каждой комнате (-contains
). Это может быстро превратиться во множество итераций и сравнений на равенство, потому что в каждой комнате, которой не назначен текущий ученик, вы повторяете всю коллекцию учеников этой комнаты, снова и снова, пока не найдете комнату для этого ученика.
Одной из простых оптимизаций, которые вы можете выполнить при выполнении линейного поиска, является сортировка искомых элементов (в этом случае свойство Students
будет упорядочено по свойству StudentNo
каждого учащегося) ...
# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
| Select-Object `
-ExcludeProperty 'students' `
-Property '*', @{
Name = 'Students'
Expression = {
$studentsText = $_.students
$studentsScriptBlock = Invoke-Expression -Command $studentsText
$studentsArray = @(& $studentsScriptBlock) `
| Sort-Object -Property @{ Expression = { $_.StudentNo } }
return $studentsArray
}
}
... и затем при поиске в той же коллекции, если вы обнаружите элемент, который на превышает тот, который вы ищете, вы знаете, что остальная часть коллекции не может содержат то, что вы ищете, и вы можете сразу прервать поиск ...
@{
Name = "roomName";
Expression = {
foreach ($r in $rooms)
{
# Requires $room.Students to be sorted by StudentNo
foreach ($roomStudentNo in $r.Students.StudentNo)
{
if ($roomStudentNo -eq $_.StudentNo)
{
# Return the matched room name and stop searching this and further rooms
return $r.roomName
}
elseif ($roomStudentNo -gt $_.StudentNo)
{
# Stop searching this room
break
}
# $roomStudentNo is less than $_.StudentNo; keep searching this room
}
}
#TODO: Return text indicating room not found?
}
}
Еще лучше: с отсортированной коллекцией вы также можете выполнить двоичный поиск , который быстрее линейного поиска *. Класс Array
уже предоставляет BinarySearch
статический метод , поэтому мы можем выполнить это и в меньшем количестве кода ...
@{
Name = "roomName";
Expression = {
foreach ($r in $rooms)
{
# Requires $room.Students to be sorted by StudentNo
if ([Array]::BinarySearch($r.Students.StudentNo, $_.StudentNo) -ge 0)
{
return $r.roomName
}
}
#TODO: Return text indicating room not found?
}
}
Однако, я бы решил эту проблему, используя [HashTable]
, сопоставляющий StudentNo
с комнатой. Для построения [HashTable]
требуется небольшая предварительная обработка, но это обеспечит постоянный поиск при поиске комнаты для студента.
function GetRoomsByStudentNoTable()
{
$table = @{ }
foreach ($room in $rooms)
{
foreach ($student in $room.Students)
{
#NOTE: It is assumed each student belongs to at most one room
$table[$student.StudentNo] = $room
}
}
return $table
}
# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
| Select-Object `
-ExcludeProperty 'students' `
-Property '*', @{
Name = 'Students'
Expression = {
$studentsText = $_.students
$studentsScriptBlock = Invoke-Expression -Command $studentsText
$studentsArray = @(& $studentsScriptBlock)
return $studentsArray
}
}
# Replace the [String] property "StudentNo" with an [Int32] property of the same name
$students = Import-Csv students.csv `
| Select-Object `
-ExcludeProperty 'StudentNo' `
-Property '*', @{
Name = 'StudentNo'
Expression = { [Int32] $_.StudentNo }
}
$roomsByStudentNo = GetRoomsByStudentNoTable
$combined = $students `
| Select-Object -Property `
'StudentNo', `
'PreferredSurname', `
'PreferredFirstnames', `
'UPN', `
@{
Name = "roomName";
Expression = {
$room = $roomsByStudentNo[$_.StudentNo]
if ($room -ne $null)
{
return $room.roomName
}
#TODO: Return text indicating room not found?
}
}
Вы можете улучшить попадание здания $roomsByStudentNo
, выполнив это одновременно с импортом rooms.csv
...
# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
| Select-Object `
-ExcludeProperty 'students' `
-Property '*', @{
Name = 'Students'
Expression = {
$studentsText = $_.students
$studentsScriptBlock = Invoke-Expression -Command $studentsText
$studentsArray = @(& $studentsScriptBlock)
return $studentsArray
}
} `
| ForEach-Object -Begin {
$roomsByStudentNo = @{ }
} -Process {
foreach ($student in $_.Students)
{
#NOTE: It is assumed each student belongs to at most one room
$roomsByStudentNo[$student.StudentNo] = $_
}
return $_
}
* За исключением небольших массивов