65.9K
CodeProject 正在变化。 阅读更多。
Home

NASA 航天飞机电视节目表传输到 Outlook 日历

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (9投票s)

2007年12月5日

GPL3

15分钟阅读

viewsIcon

72103

downloadIcon

1367

NASA航天飞机任务时间表读取NASA发布的包含航天飞机任务电视时间表的Excel文件,并提供将时间表输入和更新到Outlook日历的功能。

Screenshot - NasaTvScheduleApp

引言

我想找到一种更简单的方法来跟踪航天飞机任务的电视时间表。在我看到此应用程序的需求之前,我手动将发射和着陆等关键事件以及飞行日亮点输入到我的Outlook日历中。

NASA发布航天飞机任务的电视时间表,格式为PDF文件和Excel电子表格。此外,NASA在任务期间会频繁修改时间表。NASA通过卫星广播航天飞机任务,其节目可以在其网站和关联公司的流媒体视频、直接广播卫星(如Direct TV或DISH Network)以及许多有线电视系统上观看。

在航天飞机任务期间,NASA会频繁修改其电视时间表。一个任务可能会有十五到二十次修订,而每个任务可能多达两百个事件。这显然需要一个自动化解决方案。

自Microsoft发布Visual Studio Tools for Office以来,我看到这是一个绝佳的机会来创建一个程序,该程序读取Excel格式的电视时间表,并将时间表作为约会添加到我的Outlook日历中。

该项目也位于Microsoft的开源托管网站CodePlex上,网址为NASA Space Shuttle TV Schedule Transfer to Outlook Calendar

要求

背景

由于这是我创建的第一个使用Microsoft Visual Tools for Office的应用程序项目,我不确定应该选择哪种方式,是Microsoft.Office.Tools.Excel还是Microsoft.Office.Interop.Excel。在程序开发过程中,我一直无法使用Microsoft.Office.Tools.Excel打开文件;但是使用Microsoft.Office.Interop.Excel却成功了。

NASA的时间表中有几个值得关注的标题,NasaStsTvSchedule类会解析它们。

  1. 修订标题
  2. 日期标题
  3. 飞行日标题
  4. 事件标题

修订标题出现在任何其他标题之前,包含时间表的创建或修订日期。它出现在前几行,解释如何接收卫星信号。该类捕获修订标题以使用GetCreationRevisionDate获取任务的年份。

日期标题包含星期几、月份和日期。一个条目可能是“MONDAY, OCTOBER 22”。日期是从休斯顿(德克萨斯州)的角度来看的。ProcessDateHeader设置DateTime的月份和日期。

飞行日标题是任务的飞行日,格式为“FD #”,可选地后跟“/ FD (#+1)”。

事件标题是描述事件列的标题,包括轨道号、主题、地点、任务已用时间、中部时间、东部时间和格林威治标准时间。有时,时间表还包含莫斯科时间。从任务到任务,这些列的出现位置总是相同的。但是,当遇到事件标题时,它会为要捕获的每个信息项设置列索引。

NASA链接

下面是STS-122任务(计划于2007年12月6日发射)的修订0的截图。包含发射前条目的行已隐藏。

Screenshot of STS-122 TV Schedule rev. 0

下载文件

对于这个CodeProject文章,源文件分为两个zip文件:NasaTvScheduleSrc.zip(应用程序代码)和NasaTvScheduleSrcSetup.zip(安装代码)。

NasaTvScheduleSrc.zip包含应用程序代码。在程序开发过程中,我需要进行时区转换,而.NET 2.0在这方面有所欠缺。MSDN Base Class Library Team发布了.NET Framework中的时区[Anthony Moore],提供了我需要的解决方案,即使用他们的TimeZoneInfo类进行时区转换,该类包含在.NET 3.5中。

NasaTvScheduleSrcSetup.zip包含Setup项目以及Office 2003和Office 2007主要互操作程序集。它使用了MSDN文章使用Windows Installer部署Visual Studio 2005 Tools for Office解决方案(第一部分,共两部分)中描述的代码和技术。

NasaTvScheduleApp.zip包含用于创建应用程序的三个项目生成的输出,该应用程序在文章的原始出版物中发布。

NasaTvScheduleSetup.zip包含用于安装应用程序的安装程序。

使用NasaStsTVSchedule类

NasaStsTVSchedule使用Microsoft.Office.Interop.Excel打开Excel电子表格并获取包含时间表的单元格。

创建NasaStsTVSchedule类,该类通过以下方式读取和处理Excel电子表格中的电视时间表文件:

tvSchedule = new NasaStsTVSchedule(excelFile, viewingTimeZone);

viewingTimeZone是在SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones中的显示值。东部标准时间(Eastern Standard Time)的时区是“(GMT-05:00) Eastern Time (US & Canada)”。

ReadScheduleRow返回一个NasaStsTVScheduleEntry,其中包含添加到Outlook日历所需的以下信息:

  1. BeginDate - 开始日期和时间
  2. EndDate - 结束日期和时间
  3. Subject - 电视播放的事件
  4. Orbit - 完成的轨道数
  5. Site - 事件发生的地点
  6. FlightDay - 任务的飞行日
  7. Changed - 指示此条目是否与之前发布的日程表有所更改
  8. TypeEntry - 返回记录的ScheduleTypescheduleEntry包含调度所需的信息;而error应被检查以确定在读取Excel文件时是否发生错误

NASA电视时间表应用程序

NASA电视时间表应用程序使用NasaStsTVSchedule类读取Excel电子表格并填充DataGridView。每行都包含一个复选框,用于指示包含日程信息的那一行将被添加到Outlook的日历中作为约会。另一个复选框用于设置约会条目的提醒。

第二个DataGridView包含Outlook中的约会项,这些约会项与指定的搜索条件匹配。搜索条件是选定的类别以及选定的日期加上未来四周。复选框列指示将从Outlook中删除的约会项。

该应用程序使用Microsoft.Office.Interop.Outlook COM对象访问Outlook。

using InteropOutlook = Microsoft.Office.Interop.Outlook;

有十五个控件控制着应用程序读取时间表和管理Outlook中的约会。

  • Excel控件
    1. 打开NASA电视时间表 - 打开Excel文件并填充Excel DataGridView
    2. 全选 - 选择Excel DataGridView中的所有行。
    3. 取消全选 - 取消选择Excel DataGridView中的所有行。
    4. Excel DataGridView - 包含电视时间表中的事件时间表;包括用于选择行和在将事件转移到Outlook时添加到Outlook提醒的复选框。
  • Outlook控件
    1. 任务开始日期 - DateTimePicker设置Outlook搜索约会的开始日期;结束日期是从开始日期起四周。
    2. 观看时区 - 包含可用时区的ComboBox。选定的时区用于将中部时间转换为Excel DataGridView的时间。
    3. Outlook类别 - 包含约会可以关联的类别的CheckListBox。我创建了一个名为“NASA STS TV Schedule”的类别,用于创建约会。
    4. 全选 - 选择Outlook DataGridView中的所有行。
    5. 取消全选 - 取消选择Outlook DataGridView中的所有行。
    6. 智能选择 - 使用Excel DataGridView中的第一个和最后一个条目的开始日期和时间,并在Outlook DataGridView中选择落在该时间段内的条目。
    7. 转移电视时间表 - 从Excel DataGridView时间表中选定的条目创建Outlook中的约会。
    8. 删除选定条目 - 从Outlook中删除选定的约会。
    9. 批量导入 - 打开多个时间表并将时间表转移到Outlook。
    10. 新时间表更新 - 提供了一种简单的方法来读取修订的时间表文件并更新Outlook日历。
    11. 刷新类别 - 重新加载Outlook类别。
    12. Outlook DataGridView - 包含匹配Outlook类别和日期搜索范围搜索条件的Outlook日历条目。

应用程序中有几个关键函数负责大部分工作,包括读取时间表文件和通过添加和删除约会来维护Outlook时间表。

  • OpenNasaTvSchedule()使用Microsoft OpenFileDialog选择要加载的Excel文件,并调用LoadExcelSchedule(excelSchedule)来读取时间表文件。
  • LoadExcelSchedule(excelSchedule)读取并解析Excel时间表文件,以填充包含NASA任务时间表的DataGridView
  • LoadOutlookSchedule()加载包含指定日期范围和Outlook类别的Outlook日历条目的DataGridView
  • RemoveOutlookEntries()从Outlook的日历中删除指定的条目。
  • TransferExcelToOutlook()将NASA时间表DataGridView中的指定条目添加到Outlook的日历中。
  • SmartSelect()使用NASA时间表DataGridView的第一行和最后一行来获取日期/时间范围,并使用该日期和时间来选择Outlook DataGridView中落在该日期/时间范围内的条目。
  • SelectAllExcel()选择NASA时间表DataGridView中的所有条目。UnselectAllExcel做相反的操作。
  • SelectAllOutlook()选择Outlook DataGridView中的所有条目。UnselectAllOutlook做相反的操作。

由于我保存了航天飞机任务(STS-115、STS-116、STS-117、STS-118、STS-120、STS-121和STS-122)的电视时间表及其修订版,因此BulkImport提供了一种简便的方法将任务时间表从Excel转移到Outlook。

填充Outlook DataGridView的代码

/// <summary>
/// Loads the Calendar entries from Outlook based
/// on the selected date + LookAheadWeeks (from the Settings)
/// weeks and categories selected
/// </summary>
protected void LoadOutlookSchedule()
{
    dgvOutlook.Rows.Clear();

    InteropOutlook.ApplicationClass outlook = null;
    InteropOutlook.NameSpace nmOutlook = null;
    InteropOutlook.Folder olCalendarFolder = null;

    try
    {
        outlook = new Microsoft.Office.Interop.Outlook.ApplicationClass();

        DateTime dtStart = dtpOutlook.Value;
        dtStart = dtStart.Date;
        const int daysInWeek = 7;
        // Set an end date x weeks from the Application
        // Specified Setting of LookAheadWeeks
        DateTime dtEnd = dtStart.AddDays(daysInWeek * 
           Properties.Settings.Default.LookAheadWeeks);
        string filterDateSearchRange = "([Start] >= '" + 
               dtStart.ToString("g", CultureInfo.CurrentCulture) +
               "' AND [End] <= '" + 
               dtEnd.ToString("g", CultureInfo.CurrentCulture) + "')";
        StringBuilder filterCategories = new StringBuilder();

        string categories = GetSelectedCategories();
        // Multiple categories will be checked and separated by an OR
        if (categories.Length > 0)
        {
            string[] category = categories.Split(';');
            int indexCategories;
            int maxCategories = category.GetUpperBound(0);
            int lowCategories = category.GetLowerBound(0);

            for (indexCategories = lowCategories; indexCategories 
                             <= maxCategories; indexCategories++)
            {
                filterCategories.Append("[Categories] = " + 
                                        category[indexCategories]);
                //  If not the only category and not the last category
                if ((lowCategories != maxCategories) && 
                    (indexCategories < maxCategories))
                {
                    filterCategories.Append(" OR ");
                }
            }
        }

        string filterCalendar = filterDateSearchRange;
        // Put the date range search and categories search together
        if (filterCategories.Length > 0)
        {
            filterCalendar += " AND (" + filterCategories.ToString() + ")";
        }

        filterCategories = null;
        nmOutlook = outlook.GetNamespace("MAPI");
        //  Ralph Hightower - 20071104
        //  FolderClass, ItemClass, and AppointmentItemClass do not appear to work
        //  Use Folder, Item, and AppointmentItem instead
        //InteropOutlook.FolderClass olCalendarFolder = 
        //    nmOutlook.GetDefaultFolder(
        //     Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderCalendar)
        //    as InteropOutlook.FolderClass;
        olCalendarFolder = nmOutlook.GetDefaultFolder(
          Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderCalendar)
          as InteropOutlook.Folder;

        dgvOutlook.SuspendLayout();
        if (olCalendarFolder != null)
        {
            //InteropOutlook.ItemsClass calendarItems = 
            //  (InteropOutlook.ItemsClass)olCalendarFolder.
            //               Items.Restrict(filterCalendar);
            InteropOutlook.Items calendarItems = 
              (InteropOutlook.ItemsClass)
              olCalendarFolder.Items.Restrict(filterCalendar);
            calendarItems.Sort("[Start]", Type.Missing);
            foreach (InteropOutlook.AppointmentItem apptItem in calendarItems)
            {
                dgvOutlook.Rows.Add(false, apptItem.Start, 
                      apptItem.End, apptItem.Subject, apptItem.Location);
            }
        }
    }
    catch (COMException comExp)
    {
        MessageBox.Show(comExp.Message + comExp.StackTrace, 
            Properties.Resources.ERR_COM_EXCEPTION,
            MessageBoxButtons.OK, MessageBoxIcon.Exclamation, 
            MessageBoxDefaultButton.Button1, (MessageBoxOptions)0);
        if (Properties.Settings.Default.CopyExceptionsToClipboard)
            Clipboard.SetText(comExp.Message + comExp.StackTrace, 
                              TextDataFormat.Text);
        }
    finally
    {
        dgvOutlook.ResumeLayout();
        dgvOutlook.Refresh();
        olCalendarFolder = null;
        nmOutlook = null;
        outlook = null;
    }
}

以下是将时间表添加到Outlook的代码

/// <summary>
/// Adds the Appointment to the Outlook Calendar
/// </summary>
/// <param name="nasaTVSchedule">Class containing
///   the information for the appointment item</param>
/// <param name="reminder">Set a reminder if true</param>
/// <param name="categories">Outlook
///   categories to file this appointment under</param>
/// <param name="outlook">The Outlook application</param>
private void AddAppointment(NasaStsTVScheduleEntry nasaTVSchedule, 
        bool reminder, string categories,
        InteropOutlook.ApplicationClass outlook)
{
    try
    {
        string selectedCategories = categories.Replace(";", ", ");

        InteropOutlook.AppointmentItem appt = 
            outlook.CreateItem(
            Microsoft.Office.Interop.Outlook.OlItemType.olAppointmentItem)
            as InteropOutlook.AppointmentItem;
        appt.Start = nasaTVSchedule.BeginDate;
        appt.End = nasaTVSchedule.EndDate;
        appt.Subject = nasaTVSchedule.Subject;
        appt.Location = nasaTVSchedule.Site;
        appt.BusyStatus = Microsoft.Office.Interop.Outlook.OlBusyStatus.olFree;
        appt.Categories = selectedCategories;
        appt.ReminderSet = reminder;
        if (reminder)
            appt.ReminderMinutesBeforeStart = 15;
        appt.Importance = 
         Microsoft.Office.Interop.Outlook.OlImportance.olImportanceNormal;
        appt.BusyStatus = 
         Microsoft.Office.Interop.Outlook.OlBusyStatus.olFree;

        appt.Save();
        nasaTVSchedule = null;
    }
    catch (COMException comExp)
    {
        MessageBox.Show(comExp.Message + comExp.StackTrace, 
            Properties.Resources.ERR_COM_EXCEPTION,
            MessageBoxButtons.OK, MessageBoxIcon.Exclamation, 
            MessageBoxDefaultButton.Button1, (MessageBoxOptions)0);
        if (Properties.Settings.Default.CopyExceptionsToClipboard)
            Clipboard.SetText(comExp.Message + 
            comExp.StackTrace, TextDataFormat.Text);
    }
    finally
    {
        nasaTVSchedule = null;
    }
}

从Outlook中删除时间表条目的代码

/// <summary>
/// Deletes the Appointment from the Calendar
/// </summary>
/// <param name="dtStart">Start Time of the Appointment</param>
/// <param name="dtEnd">End Time of the Appointment</param>
/// <param name="subject">Subject of the Appointment</param>
/// <param name="site">Site of the Appointment</param>
/// <param name="outlook">Outlook Application
///                to avoid opening and closing repeatedly</param>
private void RemoveAppointment(DateTime dtStart, 
        DateTime dtEnd, string subject, string site,
        InteropOutlook.ApplicationClass outlook)
{
    //
    //    COM Exception cause: Single quotes in Subject
    //    causes RemoveAppointment to get a COM Exception
    //    in Calendar.Items.Restrict(filterAppt)
    //
    string filterAppt = "([Start] = '" + dtStart.ToString("g", 
           CultureInfo.CurrentCulture) + "') " +
           "AND ([End] = '" + dtEnd.ToString("g", 
           CultureInfo.CurrentCulture) + "') " +
           "AND ([Subject] = '" + subject.Replace("'", "''") + "') " +
           "AND ([Location] = '" + site + "')";

    InteropOutlook.NameSpace nmOutlook = null;
    InteropOutlook.Folder olCalendarFolder = null;
    try
    {
        nmOutlook = outlook.GetNamespace("MAPI");
        //  Ralph Hightower - 20071104
        //  FolderClass, ItemClass, and AppointmentItemClass do not appear to work
        //  Use Folder, Item, and AppointmentItem instead
        //  InteropOutlook.FolderClass olCalendarFolder = 
        //    nmOutlook.GetDefaultFolder(
        //    Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderCalendar)
        //    as InteropOutlook.FolderClass;
        olCalendarFolder = nmOutlook.GetDefaultFolder(
          Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderCalendar)
          as InteropOutlook.Folder;

        if (olCalendarFolder != null)
        {
            //InteropOutlook.ItemsClass calendarItems = 
            //  (InteropOutlook.ItemsClass)olCalendarFolder.Items.Restrict(filterCalendar);
            InteropOutlook.Items calendarItems = 
              (InteropOutlook.ItemsClass)olCalendarFolder.Items.Restrict(filterAppt);
            calendarItems.Sort("[Start]", Type.Missing);
            foreach (InteropOutlook.AppointmentItem apptItem in calendarItems)
            {
                apptItem.Delete();
            }
        }
    }
    catch (COMException comExp)
    {
        MessageBox.Show(comExp.Message + comExp.StackTrace, 
                   Properties.Resources.ERR_COM_EXCEPTION,
                   MessageBoxButtons.OK, MessageBoxIcon.Exclamation, 
                   MessageBoxDefaultButton.Button1, (MessageBoxOptions)0);
        if (Properties.Settings.Default.CopyExceptionsToClipboard)
            Clipboard.SetText(comExp.Message + comExp.StackTrace, TextDataFormat.Text);
    }
    finally
    {
        olCalendarFolder = null;
        nmOutlook = null;
    }
}

NasaStsTVSchedule类

为了访问Excel COM对象,NasaStsTVSchedule使用Microsoft.Office.Interop.Excel

using InteropExcel = Microsoft.Office.Interop.Excel;
NasaStsTvSchedule的公共方法 描述
NasaStsTVSchedule(string excelFile, string viewingTimeZone) 使用NASA电视时间表的文件名和观看时区初始化类。
NasaStsTVScheduleEntry ReadScheduleRow() 返回包含事件调度详情的NasaStsTVScheduleEntry
bool EOF() 如果已到达电子表格末尾,则返回true
void Close() 关闭电子表格和Excel。
bool InSpace() 如果航天飞机在轨道上,则返回true
bool IsDocked() 如果航天飞机已对接国际空间站,则返回true(与修订版时间表对接后可能不准确)。

下面是打开包含时间表的Excel文件并返回包含时间表单元格的行数组的代码。通常,NASA创建的所有电子表格文件都有一个名为“Print_Area”的名称,该名称定义了包含时间表的单元格范围。但是,有时NASA会忘记定义该名称;在这种情况下,会抛出InvalidFileFormatException。该异常会被捕获,并将错误消息通过ReadScheduleRow返回的NasaStsTvScheduleEntry传递回应用程序。

/// <summary>
/// Method to open Nasa TV Schedule using Microsoft.Office.Interop.Excel
/// </summary>
public System.Array OpenExcelFile(string NasaTVScheduleFile)
{
    System.Array printArea = null;

    SuccessfullyOpened = false;
    try
    {
        InteropExcelApplication = new Microsoft.Office.Interop.Excel.ApplicationClass();
        InteropExcelWorkbook = (InteropExcel.WorkbookClass)
            InteropExcelApplication.Workbooks.Open(NasaTVScheduleFile,
            false, true, Type.Missing, Type.Missing, Type.Missing, 
            Type.Missing, Type.Missing, Type.Missing,
            Type.Missing, Type.Missing, Type.Missing, 
            Type.Missing, Type.Missing, Type.Missing);
        InteropExcelSheets = InteropExcelWorkbook.Worksheets;
        InteropExcelWorksheet = (Microsoft.Office.Interop.Excel.Worksheet)
                                 InteropExcelSheets.get_Item(1);
        //
        //    COM Exception: Print_Area is not defined in spreadsheet
        //         (My Downloads\NASA\STS-116\tvsched_reva.xls
        //
        InteropExcelRange = InteropExcelWorksheet.get_Range(
               Properties.Resources.NASA_PRINT_AREA, Type.Missing);
        printArea = (System.Array)InteropExcelRange.Cells.Value2;
        //    Don't show Excel application
        InteropExcelApplication.Visible = false;
        SuccessfullyOpened = true;
        return (printArea);
    }
    catch (COMException comException)
    {
        if (comException.TargetSite.Name == 
            Properties.Resources.EXP_COMEXCEPTION_INTEROPEXCEL_OPENEXCELFILE_GETRANGE)
        {
            string explanation = Properties.Resources.INVALIDFILEFORMAT_NO_PRINT_AREA;
            throw new InvalidFileFormatException(String.Format(explanation, 
                                        NasaTVScheduleFile), comException);
        }
        else
        {
            if (Properties.Settings.Default.CopyExceptionsToClipboard)
                Clipboard.SetText(comException.Message + CRLF + 
                                  comException.StackTrace, TextDataFormat.Text);
            throw;
        }
    }
}

关闭Excel文件并退出Excel应用程序的代码

public void Close()
{
    if (InteropExcelWorkbook != null)
        InteropExcelWorkbook.Close(false, Type.Missing, Type.Missing);
    if (InteropExcelApplication != null)
    {
        InteropExcelApplication.DisplayAlerts = false;
        InteropExcelApplication.Quit();
    }
}

将Excel时间转换为DateTime的代码

/// <summary>
/// Formats the time of the weekdayMonthDay according to Excel method (Interop or Excel)
/// </summary>
/// <param name="row">Row of spreadsheet</param>
/// <param name="weekdayMonthDay">Column of spreadsheet</param>
/// <returns>Time as a string formatted similar to DateTime.ToString("hh:mm tt")</returns>
private string ExcelFormatTime(int row, int cell)
{
    string formattedTime = "";
    switch (ExcelTypeInterface)
    {
        case ExcelInterface.InteropExcel:
            formattedTime = DateTime.FromOADate((double)
               TvScheduleCells.GetValue(row, cell)).ToString("hh:mm tt");
            break;
        case ExcelInterface.ToolsExcel:
            formattedTime = ToolsExcelIF.FormatTime(TvScheduleCells, row, cell);
            break;
        default:
            throw new ArgumentException(Properties.Resources.ERR_EXCEL_FORMAT_TIME,
                Properties.Resources.ERR_ARGUMENT_TYPE_EXCEL);
    }
    return (formattedTime);
}

ReadScheduleRow

ReadScheduleRow在尚未打开电视时间表文件的情况下打开它,并逐行遍历电子表格。它调用DecodeScheduleRow来确定和解码各种类型的标题。如果当前行是scheduleEntry,则调用ProcessEntry来创建要返回的NasaStsTVScheduleEntry记录。

/// <summary>
/// Read MASA TV Schedule
/// Could generate an InvalidFileFormatException
/// </summary>
/// <returns>NasaStsTVScheduleEntry of scheduling information for event</returns>
public NasaStsTVScheduleEntry ReadScheduleRow()
{
    ScheduleType entryType = ScheduleType.empty;
    NasaStsTVScheduleEntry dataRow = null;
    if (!SuccessfullyOpened)
    {
        try
        {
            OpenNasaTvSchedule();
        }
        catch (InvalidFileFormatException invalidFile)
        {
            NasaStsTVScheduleEntry error = new NasaStsTVScheduleEntry(
                DateTime.MinValue, DateTime.MinValue, false,
                invalidFile.Message, 0, invalidFile.StackTrace, 
                "", ScheduleType.error);
            return (error);
        }
    }
    if (SuccessfullyOpened)
    {
        for (; !EOF() && (entryType != ScheduleType.scheduleEntry)
            && (entryType != ScheduleType.error); CurrentRow++)
        {
            //    Could get an InvalidFileFormatException exception


            try
            {
                entryType = DecodeScheduleRow(CurrentRow);
            }
            catch (InvalidFileFormatException expInvalidFileFormat)
            {
                ProcessingError = expInvalidFileFormat;
                entryType = ScheduleType.error;
            }
        }
    }
    if (entryType == ScheduleType.scheduleEntry)
    {
        CurrentRow--;   //  CurrentRow is incremented before testing
                        //  the return type of DecodeScheduleCurrentRow()


        dataRow = ProcessEntry(CurrentRow);
        if (dataRow == null)
        {
            entryType = ScheduleType.empty;
        }
        CurrentRow++;
    }
    else if (entryType == ScheduleType.error)
    {
        dataRow = new NasaStsTVScheduleEntry(DateTime.MinValue, DateTime.MinValue, false,
            ProcessingError.Message, 0, "", "", ScheduleType.error);
    }

    return (dataRow);
}

DecodeScheduleRow

DecodeScheduleRow返回当前行的记录类型。任务的年份在电子表格的前几行设置,在遇到任何标题记录之前。如果当前行的第一个单元格是一个匹配“MM/DD/YY”日期格式的字符串,则通过GetCreationRevisionDate捕获年份。ProcessCellOrbit返回记录的类型。

/// <summary>
/// Decode entries in Nasa TV Schedule Excel spreadsheet
/// Could generate an InvalidFileFormatException
/// </summary>
/// <param name="row">Row for the event to decode</param>
/// <returns>Type of event</returns>
private ScheduleType DecodeScheduleRow(int row)
{
    ScheduleType typeEntry = ScheduleType.empty;

    object cellOrbit;

    if (CurrentRow < RowCount)
    {
        //  Year has not been initialized yet
        //  A revision or creation date is required
        //  in the spreadsheet before any headers are processed
        //  The revision/creation date is in the first few lines of the spreadsheet
        if (Year == 0)
        {
            GetCreationRevisionDate();
        }
        cellOrbit = TvScheduleCells.GetValue(row, OrbitColumnHeader);
        if (cellOrbit != null)
        {
            try
            {
                typeEntry = ProcessCellOrbit(cellOrbit, row);
            }
            catch (InvalidFileFormatException)
            {
                typeEntry = ScheduleType.error;
            }
        }
        else
        {
            if (IsRowScheduleEntry(row))
                typeEntry = ScheduleType.scheduleEntry;
        }
    }
    else
    {
        IsEOF = true;
    }

    return (typeEntry);
}

ProcessCellOrbit

当当前行的第一列包含值时,调用ProcessCellOrbit,并确定当前行的记录类型。

/// <summary>
/// Process schedule entry based on the content in column 1
/// This can have many different formats
/// 1. Comments
/// 2. Header Record (ORBIT, SUBJECT, SITE, MET, C[SD]T, E[SD]T, GMT
/// 3. Date Header (DAYOFWEEK, MONTH Day)
/// 4. Flight Day Header (FD \d*)
/// 5. Definitions (not processed)
/// </summary>
/// <param name="cellOrbit">Cell Value for Orbit column</param>
/// <param name="row">Current Row</param>
/// <returns>Type of schedule for the current row</returns>
private ScheduleType ProcessCellOrbit(object cellOrbit, int row)
{
    ScheduleType typeEntry = ScheduleType.empty;
    System.Type cellOrbitType = cellOrbit.GetType();
    switch (cellOrbitType.FullName)
    {
        case "System.String":
            {
                string cellOrbitValue = (string)cellOrbit.ToString();
                //  Row contains "DEFINITION OF TERMS"
                //  which is the end of file; no schedule entries
                //  exist after this value. What remains are the
                //  definitions of the acronyms used in the schedule
                if (cellOrbitValue.Contains(Properties.Resources.NASA_DEFINITION_OF_TERMS))
                {
                    IsEOF = true;
                    typeEntry = ScheduleType.definitionOfTerms;
                }
                //  Header: ORBIT(1)   SUBJECT(3) SITE(4)   
                //               MET(6) C[SD]T(7)  E[SD]T(8)  GMT(9)
                //  Cell number in parenthesis
                else if (cellOrbitValue == Properties.Resources.NASA_ORBIT)
                {
                    typeEntry = ScheduleType.columnHeading;
                    ProcessOrbitHeader(row);
                }
                else
                {
                    if (MatchDateHeader(cellOrbitValue))
                    {
                        try
                        {
                            HeadingDate = ProcessDateHeader(cellOrbitValue);
                            typeEntry = ScheduleType.dateHeading;
                        }
                        catch (InvalidFileFormatException expInvalidFileFormat)
                        {
                            ProcessingError = expInvalidFileFormat;
                        }
                        finally
                        {
                            if (ProcessingError != null)
                                typeEntry = ScheduleType.error;
                        }
                        break;
                        //  Have the Date, no need to check any other missionDay


                    }

                    //  if a date heading wasn't found, look
                    //  for Flight Day heading (FD \d .*/ FD \d)
                    if (MatchFlightDayHeader(cellOrbitValue))
                    {
                        typeEntry = ScheduleType.flightDayHeading;
                    }
                }
            }
            break;
        //  Row containing orbit value must have a entry and central time,
        //  besides mission elapsed time and eastern time
        case "System.Double":
            {
                //  Know that Orbit column has a number
                //  Do the columns, Subject, Central Time, Eastern Time,
                //  and GMT contain String, Double, Double, Double?
                if (IsRowScheduleEntry(row))
                    typeEntry = ScheduleType.scheduleEntry;
            }
            break;
        default:
            typeEntry = ScheduleType.empty;
            break;
    }
    return (typeEntry);
}

ProcessEntry

ProcessEntry从当前行和设置日期标题记录的月份和日期的前一行收集信息。它调用ReadAhead()来获取当前事件的结束时间和日期。有些事件的持续时间比下一个计划事件长,例如宇航员睡眠时间和EVA;对于这些事件,会跳过下一个发生的事件,直到遇到匹配的结束事件。同样,有些事件的持续时间不会持续到下一个计划事件。这些事件是飞行日亮点(通常持续30到45分钟)、新闻发布会、采访和任务简报。对于这些事件,GuesstimateFixedEvents使用EventTimes数组来获取事件的持续时间。

/// <summary>
/// Creates a NasaStsTVScheduleEntry for schedule entries
/// </summary>
/// <param name="row">Row for the schedule to capture</param>
/// <returns>Event Schedule</returns>
private NasaStsTVScheduleEntry ProcessEntry(int row)
{
    //  Running into problems converting between timezones
    //  .Net does not have the capability
    //  TimeZone information is local time
    DateTime dtCentral = HeadingDate;
    DateTime dtBeginViewingTime;
    DateTime dtEndViewingTime = HeadingDate;

    Changed = false;
    //  Column 2 will contain an asterisk if an item has changed

    bool validEntry = false;

    NasaStsTVScheduleEntry entryRow = null;

    //  If there has been a flight missionDay heading process
    if (!((HeadingDate.Year == 1) && (HeadingDate.Month == 1) 
        && (HeadingDate.Day == 1)
        && (HeadingDate.Hour == 0) && (HeadingDate.Minute == 0)))
    {
        object cellTwo = TvScheduleCells.GetValue(row, 2);
        if (cellTwo != null)
        {
            System.Type cellTwoType = cellTwo.GetType();
            if (cellTwoType.FullName == Properties.Resources.SYSTEM_STRING)
            {
                string cellTwoValue = cellTwo.ToString();
                Changed = (cellTwoValue == Properties.Resources.NASA_CHANGED);
            }
        }
        Subject = GetMultiLineSubject(row);

        //  State variables for docking and in space
        //  are not reliable for schedule revisions
        //  published after launch or docking
        if (Subject.Contains(Properties.Resources.NASA_DOCKING))
        {
            if (!Subject.Contains(Properties.Resources.NASA_VTR_PLAYBACK))
            {
                Docked = true;
            }
        }
        else if (Subject.Contains(Properties.Resources.NASA_UNDOCKING) ||
        Subject.Contains(Properties.Resources.NASA_UNDOCKS))
        {
            if (!Subject.Contains(Properties.Resources.NASA_VTR_PLAYBACK))
            {
                Docked = false;
            }
        }
        else if (Subject == Properties.Resources.NASA_LAUNCH)
        {
            if (!Subject.Contains(Properties.Resources.NASA_VTR_PLAYBACK))
            {
                InOrbit = true;
            }
        }
        else if (Subject.Contains(Properties.Resources.NASA_LANDING))
        {
            if (!Subject.Contains(Properties.Resources.NASA_VTR_PLAYBACK))
            {
                InOrbit = false;
                Landed = true;
            }
        }
        if (TvScheduleCells.GetValue(row, SiteColumHeader) != null)
            Site = TvScheduleCells.GetValue(row, SiteColumHeader).ToString();
        else
        {
            if (Docked)
                Site = Properties.Resources.NASA_ISS;
            else
                Site = Properties.Resources.NASA_STS;

            if (Subject.Contains(Properties.Resources.NASA_CREW_SLEEP_BEGINS) ||
                Subject.Contains(Properties.Resources.NASA_CREW_WAKE_UP))
            {
                if (ISSCrewSleep(row) || ISSCrewWakeUp(row))
                    Site = Properties.Resources.NASA_ISS;
                if (ShuttleCrewSleep(row) || ShuttleCrewWakeUp(row))
                    Site = Properties.Resources.NASA_STS;
            }
        }
        if (TvScheduleCells.GetValue(row, MissionElapsedTimeColumnHeader) != null)
        {
            MissionElapsedTime = TvScheduleCells.GetValue(row, 
                      MissionElapsedTimeColumnHeader).ToString();
            MissionDurationTime.Set(TvScheduleCells, row, MissionElapsedTimeColumnHeader);
        }
        //  watch for "NET L" usually means "Net Landing + some time"
        if (TvScheduleCells.GetValue(row, OrbitColumnHeader) != null)
            Orbit = (System.Double)TvScheduleCells.GetValue(row, OrbitColumnHeader);
        if (TvScheduleCells.GetValue(row, FlightDayColumnHeader) != null)
            FlightDay = TvScheduleCells.GetValue(row, FlightDayColumnHeader).ToString();
        if (TvScheduleCells.GetValue(row, CentralTimeColumnHeader) != null)
        {
            CentralTime = ExcelFormatTime(row, CentralTimeColumnHeader);
            dtBeginViewingTime = ConvertFromCentralTzToViewingTz(dtCentral, CentralTime);
            dtEndViewingTime = GuesstimateFixedEvents(Subject, dtBeginViewingTime);
            //  If a special event was not found, get the start time for the next event


            if (dtEndViewingTime == DateTime.MinValue)
                dtEndViewingTime = ReadAhead();
            validEntry = true;
            //  This situation may not occur (except for STS
            //  Landing since though there are next events,
            //  the events are Net Landing + a time span
            //  If the end time occurs before the beginning time, assume 30 minutes
            if (dtBeginViewingTime > dtEndViewingTime)
                dtEndViewingTime = dtBeginViewingTime.AddHours(1);
            entryRow = new NasaStsTVScheduleEntry(dtBeginViewingTime, 
                       dtEndViewingTime, Changed, Subject,
                       Orbit, Site, FlightDay, ScheduleType.scheduleEntry);
        }
    }

    if (validEntry)
    {
        return (entryRow);
    }
    else
        return (null);
}

生成的异常

NasaStsTVSchedule基于以下两种情况之一抛出InvalidFileFormatException

  • 未找到日程修订或创建日期。需要修订或创建日期来获取任务的年份。如果在找到任何常规标题记录之前未找到日期,则会抛出InvalidFileFormatException。原因是:“在文件 {0} 中未找到创建或修订日期。无法确定任务的年份。”
  • NASA时间表通常有一个名为“Print_Area”的单元格范围,该范围定义了时间表的区域。如果电子表格中未定义名为Print_Area的单元格范围,则会抛出InvalidFileFormatException。原因是:“在NASA STS电视时间表文件 {0} 中未定义范围Print_Area”。

NasaStsTvScheduleEntry

NasaStsTvScheduleEntry包含在Outlook中创建调度所需的所有信息。

NasaStsTvScheduleEntry的公共方法、属性 描述
NasaStsTvScheduleEntry(DateTime entryBeginDateTime, DateTime entryEndDateTime, bool entryRevised, string entrySubject, double entryOrbit, string entrySite, string entryFlightDay, ScheduleType entryType) 事件日程表的构造函数,包含开始和结束日期时间、修订指示符、主题、事件的轨道号、事件地点、飞行日和条目类型。
DateTime BeginDate 事件开始的日期和时间。
DateTime EndDate 事件结束的日期和时间。
string Subject 事件主题。
double Orbit 返回事件的轨道号。
string Site 事件的地点。
string FlightDay 事件发生的飞行日。
bool Changed 指示事件是否与之前发布的日程表有所更改。
ScheduleType TypeEntry 返回条目类型:columnHeadingdateHeadingflightDayHeadingscheduleEntryemptyerror

ScheduleType

/// <summary>
/// Enum to interpret the different types of rows
/// in the Space Shuttle TV Schedule spreadsheet
/// empty: blank
/// columnHeading: for the row containing the column header for the schedule events
/// dateHeading: changes the Date
/// flightDayHeading: header for the Flight Day
/// scheduleEntry: the event with start and end times
///        The Subject may be on multiple lines in
///        the same column, other column entries will be
///        blank if the subject is continued
/// definitionOfTerms: end of file, definitions are skipped
/// </summary>
public enum ScheduleType
{
    empty, columnHeading, dateHeading, flightDayHeading, scheduleEntry,
        definitionOfTerms, error
};

关注点

时区转换

时间表是从休斯顿(德克萨斯州)的角度发布的,因为那是约翰逊航天中心的所在地。我不想进行日期计算来推进日期,因为当东部时间午夜到达时,休斯顿(德克萨斯州)仍然是晚上11点。我决定让程序使用时区转换。那时我发现.NET 2.0无法处理时区之间的转换!你可以轻松地在本地时区和协调世界时之间进行转换;但是.NET 2.0中没有在时区之间进行转换的方法。我在MSDN上搜索,并找到了Base Class Library团队在其博客上发布的TimeZoneInfo。 .NET 3.5框架中有TimeZoneInfo,但在我开发此程序时,它处于beta发布状态。

从夏令时切换到标准时

在STS-120任务期间,从2007年10月23日到2007年11月7日,在“不存在时间”的这一个小时内安排了事件,即从夏令时转换为标准时间的过渡时段,在2007年11月4日。休斯顿(德克萨斯州)的时间仍然是夏令时,而南卡罗来纳州是标准时。我必须在将中部时间转换为查看者时区(东部时间)的例程中添加一个特殊情况。

/// <summary>
/// The Nasa TV Schedule is Houston-centric.
/// This is an easy method to convert from Central to 
/// other time zones
/// There is a kludge for that 2 AM hour that
/// does not occur when Daylight Savings Time ends 
/// and Standard Time begins for Eastern Time
///
/// Uses TimeZoneInfo developed by Microsoft MSDN BCL Team
/// </summary>
/// <param name="dtConvert">Date of the event in Central Time Zone</param>
/// <param name="timeOfday">Time of the event in Central Time Zone</param>
/// <returns>DateTime in Viewer's Time Zone</returns>
private DateTime ConvertFromCentralTzToViewingTz(DateTime dtConvert, 
                                                 string timeOfday)
{
    string convertTime = timeOfday.Trim();
    DateTime dtCentralTZ = dtConvert.Date;
    DateTime dtTimeOfDay = DateTime.Parse(convertTime, CultureInfo.CurrentCulture);
    dtCentralTZ = dtCentralTZ.Add(dtTimeOfDay.TimeOfDay);
    DateTime dtViewingTZ = TimeZoneInfo.ConvertTimeZoneToTimeZone(dtCentralTZ, 
        JohnsonSpaceCenterTZ, ViewingTimeZoneTZ);
    // Kludge for Eastern Daylight Time transition to Eastern Standard Time
    if ((dtCentralTZ.Hour == dtViewingTZ.Hour) && 
        (ViewingTimeZoneTZ.DisplayName == Properties.Resources.TZ_US_EASTERN))
        dtViewingTZ = dtViewingTZ.AddHours(1);

    return (dtViewingTZ);
}

正则表达式

我曾有过使用正则表达式的经验,那是在我编写Unix系统程序时。在这个程序中,正则表达式得到了充分利用。

正则表达式用于以下方面:

  1. 日期标题,用于捕获事件的月份和日期。
  2. 修订日期,用于捕获任务的年份。
  3. EVA活动 - 开始和结束。
  4. 飞行日标题
  5. 飞行日亮点 - 当天事件的回顾。
  6. ISS宇航员睡眠活动 - 睡眠开始和起床。
  7. 航天飞机宇航员睡眠活动 - 睡眠开始和起床。

正则表达式的一个创新用途是在SubjectVerbPatternMatch函数中,它用于配对开始和结束活动,例如“SHUTTLE CREW SLEEP BEGINS”与“SHUTTLE CREW WAKE UP”。同样,该函数也用于匹配ISS宇航员的睡眠时段和ISS宇航员的起床时间,并将“EVA BEGINS”与“EVA ENDS”配对。对于宇航员睡眠时段开始或EVA开始的ReadAhead逻辑将搜索宇航员起床时间或EVA结束时间来获取结束时间。主题是< Shuttle > < ISS >或< Eva >,活动是“crew sleep begins”和“crew wake up”,或者EVA活动是“begins”或“ends”。

正则表达式#1和#2看起来相同,但是,在#1中,Shuttle是必需的,而ISS是可选的;这用于航天飞机宇航员的睡眠时段和起床时间。在正则表达式#2中,Shuttle是可选的,而ISS是必需的,用于ISS宇航员的睡眠时段和宇航员起床时间。以下是使用的正则表达式:

  1. (?<Shuttle>ATLANTIS|DISCOVERY|ENDEAVOUR)(?:\s*/?\s*)?(?<ISS >ISS)?(?:\s*)(?<Activity>CREW SLEEP BEGINS|CREW WAKE UP)
  2. (?<Shuttle>ATLANTIS|DISCOVERY|ENDEAVOUR)?(?:\s*/?\s*)?(?<ISS >ISS)(?:\s*)(?<Activity>CREW SLEEP BEGINS|CREW WAKE UP)
  3. (?<Eva>EVA)\s+(?<Number>#\d+)\s+(?<Activity>BEGINS|ENDS)
/// <summary>
/// Helper method used by:
/// 1. ShuttleCrewSleepBegins
/// 2. ShuttleCrewWakeup
/// 3. ISSCrewSleepBegins
/// 4. ISSCrewWakeUp
/// 5. EVABegins
/// 6. EVAEnds
/// </summary>
/// <param name="rgSubjectVerbPattern">Regular expression
///   for required rgSubjectVerbPattern: 
//    Shuttle or ISS</param>
/// <param name="subject">Crew: Shuttle or ISS</param>
/// <param name="verb">CREW WAKE UP or CREW SLEEP BEGINS</param>
/// <param name="row">Row in TvScheduleCells with Subject to match</param>
/// <returns>true if Required Crew is in the desired Sleep or Wake Activity</returns>
private bool SubjectVerbPatternMatch(Regex rgSubjectVerbPattern,  
             string subject, string verb, int row)
{
    string entry = TvScheduleCells.GetValue(row, SubjectColumnHeader).ToString();

    Match mtchSubjectVerb = rgSubjectVerbPattern.Match(entry);

    GroupCollection grpcollSubjectVerb = mtchSubjectVerb.Groups;

    bool matchSubjectVerb = grpcollSubjectVerb[subject].Success &&
        (grpcollSubjectVerb[Properties.Resources.IX_ACTIVITY].Success &&
        (grpcollSubjectVerb[Properties.Resources.IX_ACTIVITY].ToString() == verb));

    return (matchSubjectVerb);
}

历史

  1. 2007年12月3日。初始版本。
  2. 2007年12月21日。
    • 修复了在航天飞机宇航员和ISS宇航员的起床时间不同时,找不到航天飞机宇航员起床电话的bug。
    • 根据CodeProject读者的建议,添加了“新时间表更新”。这是一个绝佳的建议,使得程序更容易使用。
    • 添加了安装设置项目和用于下载的设置应用程序。
  3. 2007年12月23日
    • 修复了正则表达式HTML标记的格式。
  4. 2007年12月29日
    • 添加了关于InvalidFileFormatException的部分。
    • 修复了正则表达式中Endeavour的拼写错误,在ReadAhead(...)之后恢复了任务月份和年份;收紧了新年滚动的规则。
    • 更新了源代码、安装代码和编译代码。
  5. 2008年12月01日
    • 添加了最新下载链接
© . All rights reserved.