' MPX-Main.MMScript : Export/Import MPX files
' Copyright(c) Mindjet LLC 2002,2003  All Rights Reserved

' Version History
' V2.0	8 Aug 2003	First release on X5
' V2.0.1 26 Sep 2003	CR 10527, truncate textnotes to 3072 chars on output
' V2.0.2 26 Sep 2003	CR 10522, correct test for topic hyperlink
' V2.0.3 26 Sep 2003	CRs 10518, 10519  Look for MPX magic field in first 5 records, abandon if not found

'#Uses "Language.MMBas" ' language file placed by installation script
'#Uses "MPX-Project-X5.Cls" ' mpxProject object Class module
'#Uses "MPX-Windows.MMBas" ' Windows API constants
'#Uses "MPX-Tasks-X5.Cls" ' mpxTasks collection class module
'#Uses "MPX-Task-X5.Cls" ' mpxTask object class module
'#Uses "MPX-Resources-X5.Cls" ' mpxResources collection Class module
'#Uses "MPX-Resource-X5.Cls" ' mpxResource object Class module
'#Uses "MPX-Assignments-X5.Cls" ' mpxAssignments collection class module
'#Uses "MPX-Assignment-X5.Cls" ' mpxAssignment object Class module

Option Explicit

Const b_Debug = False ' Debug mode: set this to True to cause a stop at an error line
Const b_UseTransaction = True ' use transaction object to write to map
Const s_Owner = "Mindjet"
Dim s_AppName As String ' application name
Const s_AppVer = "V2.0.1"
Const s_ArgExport = "/Export" ' internal command argument to export from MM to MPX (not translated)
Const s_ArgImport = "/Import" ' internal command argument to import from MM to MPX (not translated)
Const s_ArgPrefs = "/Prefs" ' internal command argument to display Preferences dialogue (not translated)
Const s_ArgHelp = "/Help" ' internal command argument to display Help (not translated)
Dim s_HelpFile As String ' Help / RMF document
Dim s_HelpMap As String ' Help map
Const b_PermapPrefs = True ' store preferences per map
Const b_RegistryPrefs = True ' store default preferences in the Registry
Const i_MaxPrefLen = 3 ' maximum number of digits in preference size count
Const s_KeyPrefs = "Preferences" ' Preferences name in Registry (not translated)
Const s_KeyMRU = "MRU" ' Most recently used filename stored in Registry (not translated)
Dim s_WinBuf As String ' buffer used for reading info from Windows
Const i_MaxNotesLen = 3072 ' CR 10527

' Map Data
Dim m_wkgDoc As Document ' working document
Dim m_wkgTopic As Topic ' working topic
Dim WithEvents mTrans As Transaction ' transaction object used to publish maps
Const s_ResourceSep = ";" ' resource separator character in MM2002

' Project model
Public o_Project1 As mpxProject ' Project object; only one defined

' User Preferences
Dim b_ScheduleFromStart As Boolean ' Schedule from project start date
Dim b_CheckExportable As Boolean ' check for any unexportable data before export
Dim b_ValidateProject As Boolean ' validate project map before export, only output valid project info
Dim i_WorkingDaysPerWeek As Integer ' working days per week
Dim i_DefaultPriority As Integer ' default task priority
Dim s_DefaultResource As String ' default resource used if none defined
Dim i_DoRelationships As Integer ' 0 = ignore, 1 = import/export predecessor tasks, 2 = import/export successor tasks
Dim b_IncludeProjectRollup As Boolean ' include a project-level rollup task when exporting (not exposed in UI)
Dim b_UseMilestoneCode As Boolean ' use a special code to mark Milestones
Dim i_ImportSchedule As Integer ' 0 = import constraints only, 1 = import schedule info
Dim i_DefaultRelationship As Integer ' Task relationship types: 0 = SS, 1 = SF, 2 = FS, 3 = FF
Dim i_HoursPerDay As Integer ' Hours per day, required for X5 (not a MM-configurable value)

' Internal options
Dim s_DateSep As String ' date separator character
Dim i_DateFormat As Integer ' Date format numbered per MPX date formats
Dim i_DateOrder As Integer ' Date field order numbered per MPX date order
Dim s_LongMonths(1 To 12) As String ' Long month names array
Dim s_ShortMonths(1 To 12) As String ' Short month names array
Dim s_DecimalSep As String ' Decimal separator character (e.g. ".")
Dim s_LocalDecimalSep As String ' Local decimal separator character (e.g. ".")

' MPX file Data
Const s_MPXExt = "mpx" ' default MPX file extension
Const s_MPXExtLong = "*.mpx" ' default MPX file extension
Const s_Badchars = "*?\/:;.""" & Chr(39) ' list of characters disallowed in filenames
Dim s_MPXsep As String ' list separator character
Dim s_MPXRecords As String ' Complete set of MPX file records
Dim i_MPXRecordCount As Integer ' MPX Record count
Dim s_MPXRecord As String ' MPX file record
Dim i_MPXField As Integer ' MPX Field counter
Dim v_9PriorityNames As Variant ' array of priority names for 9-level scheme
Dim v_DurationNames As Variant ' array of duration interval unit suffixes

' Field positions of the MPX fields used by MindManager
Dim i_FieldPos_ResourceName As Integer ' Resource Name(1) field position
Dim i_FieldPos_ResourceID As Integer ' Resource ID(40) field position
Dim i_FieldPos_TaskID As Integer ' Task ID (90) field position
Dim i_FieldPos_TaskName As Integer ' Task Name (1) field position
Dim i_FieldPos_TaskOutlineLevel As Integer ' Task Outline Level (3) field position
Dim i_FieldPos_TaskPriority As Integer ' Task Priority (95) field position
Dim i_FieldPos_TaskPercentComplete As Integer ' Task Percent Complete (44) field position
Dim i_FieldPos_TaskPercentWorkComplete As Integer ' Task % Work Complete (25) field position
Dim i_FieldPos_TaskStartDate As Integer ' Task Start Date (50) field position
Dim i_FieldPos_TaskFinishDate As Integer ' Task Finish Date (51) field position
Dim i_FieldPos_TaskEarlyStartDate As Integer ' Task Early Start Date (52) field position
Dim i_FieldPos_TaskEarlyFinishDate As Integer ' Task Early Finish Date (53) field position
Dim i_FieldPos_TaskLateStartDate As Integer ' Task Late Start Date (54) field position
Dim i_FieldPos_TaskLateFinishDate As Integer ' Task Late Finish Date (55) field position
Dim i_FieldPos_TaskDuration As Integer ' Task Duration (40) field position
Dim i_FieldPos_TaskMilestone As Integer ' Task Milestone flag (81) field position
Dim i_FieldPos_TaskActualDuration As Integer ' Task Actual Duration (42) field position
Dim i_FieldPos_TaskConstraintDate As Integer ' Task Constraint Date (68) field position
Dim i_FieldPos_TaskConstraintType As Integer ' Task Constraint Type (91) field position
Dim i_FieldPos_TaskPredecessors As Integer ' Task Predecessor list (70)
Dim i_FieldPos_TaskSuccessors As Integer ' Task Successor list (71)

' MPX file definitions
' Character constants
Const mpx_Magic = "MPX" ' magic first field for MPX file
Const mpx_Cnewline = Chr(127) ' newline character
Const mpx_Cquote = """" ' quote character
Const mpx_CListSep = "," ' default list separator character

' Record Numbers
Const mpx_RN_FileCreation = "" ' File creation record (no record number)
Const mpx_RN_Currency = 10 ' currency settings
Const mpx_RN_Default = 11 ' default settings
Const mpx_RN_DateTime = 12 ' date and time settings
Const mpx_RN_BaseCalDef = 20 ' base calendar definition
Const mpx_RN_BaseCalHours = 25 ' base calendar hours
Const mpx_RN_BaseCalExcep = 26 ' exceptions to base calendar
Const mpx_RN_ProjectHeader = 30 ' Project header record
Const mpx_RN_TextResources = 40 ' Text version of resources table definition
Const mpx_RN_NumericResources = 41 ' Numeric version of resources table definition
Const mpx_RN_Resource = 50 ' Resource record
Const mpx_RN_ResourceNotes = 51 ' Notes for a resource record
Const mpx_RN_ResourceCalDef = 55 ' Resource calendar definition
Const mpx_RN_ResourceCalHours = 56 ' Resource calendar hours
Const mpx_RN_ResourceCalExcep = 57 ' Exceptions to the Resource Calendar
Const mpx_RN_TextTaskTableDef = 60 ' Text Task Table definitions
Const mpx_RN_NumericTaskTableDef = 61 ' Numeric Task Table definitions
Const mpx_RN_Task = 70 ' Task record
Const mpx_RN_TaskNotes = 71 ' Task notes
Const mpx_RN_RecurringTask = 72 ' Recurring task information
Const mpx_RN_ResourceAssignment = 75 ' Resource assignment for a task
Const mpx_RN_AssignmentWorkgroup = 76 ' Assignment Workgroup fields
Const mpx_RN_ProjectNames = 80 ' Project names
Const mpx_RN_DDEOLELinks = 81 ' DDE and OLE links
Const mpx_RN_Comment = 0 ' comment field

' Data for managing MindManager version checking
Const s_MinMMVersion = "5.0.830" ' minimum required MM version
Const s_MaxMMVersion = "6.9.9999" ' maximum allowed MM version
Const s_forcedVersion  = "" ' Override actual MM version: e.g. set to "v4.0.60"
Const s_MaskChr = "x" ' mask character used in max version mask
Const i_Parts = 3 ' number of parts in the Version number
Const s_SepChr = "." ' separator character used in version numbers
Dim v_MMVersions() As Variant ' public parsed versions; (1) = Major, (2) = Minor, (3) = Build
' A registry key holds the MM version about which the user does not want to be warned
Const s_KeySection = "Settings"
Const s_KeyWarn = "dontWarnMeAbout"
Const i_CancelErr = 10031 ' error code used for cancellation

Sub Main
Dim v_Command As Variant ' command argument
Dim s_PopUp() As String ' popup menu
Dim s_1 As String

	' Establish globals
	Call p_SetWinBuf(s_WinBuf) ' set up buffer for reading Windows API
	s_AppName = T_0 ' application name
	' no help file
	's_HelpFile = T_1 ' Help file name
	s_HelpMap = T_2 ' Demo map name

	' Debug housekeeping and QA
	Debug.Clear
	If b_Debug Then
		Beep
		s_1 = s_AppName & " " & s_AppVer & " at " & Format(Now())
		Debug.Print s_1
		Debug.Print String(Len(s_1), "=")
		MsgBox("DEBUG MODE", vbExclamation, s_AppName & " " & s_AppVer)
		If Len(s_forcedVersion) > 0 Then _
			MsgBox("MM Version forced to " & s_forcedVersion, vbExclamation, s_AppName & " " & s_AppVer)
	Else
		On Error GoTo QuitOnError ' standard error handling
	End If

	If f_CheckMMVersion(s_MinMMVersion, s_MaxMMVersion) <> vbOK Then Exit Sub ' not compatible

	v_Command = UCase(Command) ' read command line argument
	If Len(v_Command) = 0 Then ' no command argument supplied
		ReDim s_PopUp(0 To 3) ' 4 choices
		s_PopUp(0) = s_ArgExport ' Export tree to MPX
		s_PopUp(1) = s_ArgImport ' Import MPX to tree
		s_PopUp(2) = s_ArgPrefs ' Edit preferences
		s_PopUp(3) = s_ArgHelp ' Display help
		v_Command = ShowPopupMenu(s_PopUp()) ' display menu & fetch result
		If v_Command >= 0 Then
			v_Command = s_PopUp(v_Command) ' read command
		Else
			v_Command = "" ' no command
		End If
	End If
	' Preassign m_WkgDoc without raising an error
	Set m_wkgDoc = Nothing ' assume none
	If Documents.Count > 0 Then Set m_wkgDoc = ActiveDocument

	Select Case UCase(v_Command)
		Case UCase(s_ArgExport) ' export map to MPX
			Set o_Project1 = New mpxProject ' initialise new project object
			Call p_InitPrefs(m_wkgDoc) ' initialise preferences
			Call p_ReadPrefs(m_wkgDoc) ' Overwrite them from storage
			If f_ImportMap() Then
				If f_ExportMPX() Then ' import from map, then export from tables to MPX
					MsgBox(Replace(T_13, "%1", T_0exp), vbInformation, T_0exp)
				End If
			End If

		Case UCase(s_ArgImport) ' import MPX to map
			Set o_Project1 = New mpxProject ' initialise new project object
			If (m_wkgDoc Is Nothing) Then Set m_wkgDoc = Documents.Add(True) ' start new doc
			Call p_InitPrefs(m_wkgDoc) ' initialise preferences
			Call p_ReadPrefs(m_wkgDoc) ' Overwrite them from storage
			If f_ImportMPX() Then
				If f_ExportMap2() Then ' import from MPX, then export from tables to map
					MsgBox(Replace(T_13, "%1", T_0imp), vbInformation, T_0imp)
				End If
			End If

		Case UCase(s_ArgPrefs) ' display preferences dialogue
			Set o_Project1 = New mpxProject ' initialise new project object
			Call p_InitPrefs(m_wkgDoc) ' initialise preferences
			Call p_ReadPrefs(m_wkgDoc) ' Overwrite them from storage
			While True
				If Not f_Preferences(m_wkgDoc) Then Exit While ' edit preferences
				Call p_InitPrefs(m_wkgDoc) ' re-initialise preferences and redisplay
			Wend

		Case UCase(s_ArgHelp) ' display help
			If Len(s_HelpFile) > 0 Then Call f_OpenDoc(MacroDir & "\" & s_HelpFile)
			If Len(s_HelpMap) > 0 Then Documents.Open(MacroDir & "\" & s_HelpMap, "", True).Activate ' open Help map

	End Select
	GoTo CleanUp ' finished

QuitOnError:
	Select Case Err.Number
		Case i_CancelErr ' user clicked Cancel or quit the dialog
			Err.Clear
			Resume Next ' ignore this "error" and continue
		Case Else ' Show an error box with the message
			Call p_DisplayError(False) ' display message without details
			Err.Clear
			SaveSetting(s_AppName, s_KeySection, s_KeyWarn, "") ' revoke version warning suppression
	End Select

' Major objects are released at cleanup in case an error got us here
CleanUp:
	If Not (m_wkgTopic Is Nothing) Then
		Set m_wkgTopic = Nothing
	End If
	Set mTrans = Nothing
	Set m_wkgDoc = Nothing
	Set o_Project1 = Nothing

End Sub

Sub p_InitPrefs(ByVal m_Doc As Document)
' Set up default Preferences
Dim i_1 As Integer

	' Set up priority mapping array: this is statically defined
	' Refer to the data mapping description for details
	v_9PriorityNames = Array(T_201, T_202, T_203, T_204, T_205, T_206, T_207, T_208, T_209)
	' Set up duration suffixes
	v_DurationNames = Array(T_220, T_221, T_222, T_223) ' mins, hours, days, weeks
	' Set up decimal place delimiter
	Call GetLocaleInfo(0, LOCALE_SMONDECIMALSEP, s_WinBuf, Len(s_WinBuf)) ' Use currency decimal separator
	s_LocalDecimalSep = f_ReadWinBuf(s_WinBuf) ' MPX file reports currency separator
	s_DecimalSep = s_DecimalSep ' remote decimal separator defaults to local one to start with

	' Set up MPX file defaults
	s_MPXsep = mpx_CListSep ' default list separator character
	Call GetLocaleInfo(0, LOCALE_SDATE, s_WinBuf, Len(s_WinBuf)) ' Date separator
	s_DateSep = f_ReadWinBuf(s_WinBuf)
	Call GetLocaleInfo(0, LOCALE_IDATE, s_WinBuf, Len(s_WinBuf)) ' Date field order
	i_DateOrder = CInt(f_ReadWinBuf(s_WinBuf))
	i_DateFormat = 1 ' default to format 1 (equivalent to short date format)
	' Set up month name arrays
	For i_1 = 0 To 11
		Call GetLocaleInfo(0, LOCALE_SMONTHNAME1 + i_1, s_WinBuf, Len(s_WinBuf)) ' Long month name
		s_LongMonths(i_1 + 1) = UCase(f_ReadWinBuf(s_WinBuf))
	Next i_1
	For i_1 = 0 To 11
		Call GetLocaleInfo(0, LOCALE_SABBREVMONTHNAME1 + i_1, s_WinBuf, Len(s_WinBuf)) ' Short month name
		s_ShortMonths(i_1 + 1) = UCase(f_ReadWinBuf(s_WinBuf))
	Next i_1

	b_ScheduleFromStart = True ' Schedule from project start date
	b_ValidateProject = False ' Don't validate unless required
	b_CheckExportable = False ' Assume we are not checking for exportable data
	i_WorkingDaysPerWeek = 5 ' Assume 5 working days per week
	i_HoursPerDay = 8 ' Assume 8 hours per day
	i_DefaultPriority = 5 ' default to middle priority for tasks
	s_DefaultResource = T_5 ' ""
	i_DoRelationships = 1 ' default to importing and exporting Predecessors
	b_IncludeProjectRollup = True ' default to including a project-level rollup task (not shown in UI)
	b_UseMilestoneCode = True ' Use special code to mark milestones
	i_ImportSchedule = 0 ' default to importing constraining data only
	i_DefaultRelationship = 2 ' Default to Finish-to-Start relationship

End Sub

Function f_Preferences(ByVal m_Doc As Document) As Boolean
' Display Preferences dialogue and allow user to change Preferences
' Returns False if Prefs dialogue closed, True to keep it open
Dim i_1 As Integer
Dim s_DefaultPriority() As String
Dim s_DefaultRelationship() As String
Dim s_DaysPerWeek(0 To 6) As String
Dim s_HoursPerDay(0 To 11) As String
Dim s_source As String ' Preferences source
Dim s_picture As String ' Clock code picture source

	' Set up Days Per Week array
	For i_1 = 1 To 7
		s_DaysPerWeek(i_1 - 1) = CStr(i_1) ' Write 1 - 7 in array
	Next i_1

	' Set up hours per day
	For i_1 = 1 To 12
		s_HoursPerDay(i_1 - 1) = CStr(i_1) ' write 1 - 12 in array
	Next i_1

	' Set up default priority array
	If i_DefaultPriority < 1 Then i_DefaultPriority = 1
	ReDim s_DefaultPriority(0 To 8)
	For i_1 = 0 To 8
		s_DefaultPriority(i_1) = CStr(i_1 + 1) ' write 1 - 9 in array
	Next i_1
	If i_DefaultPriority > 9 Then i_DefaultPriority = 9 ' check upper limit

	' Set up default task relationship array
	If i_DefaultRelationship < 0 Then i_DefaultRelationship = 0
	If i_DefaultRelationship > 3 Then i_DefaultRelationship = 3
	ReDim s_DefaultRelationship(0 To 3)
	s_DefaultRelationship(0) = f_ConvertTRD(0) ' Start to Start
	s_DefaultRelationship(1) = f_ConvertTRD(1) ' Start to Finish
	s_DefaultRelationship(2) = f_ConvertTRD(2) ' Finish to Start (default)
	s_DefaultRelationship(3) = f_ConvertTRD(3) ' Finish to Finish

	' Identify source of preferences
	If m_Doc Is Nothing Then
		s_source = T_100a ' deriving preferences from defaults (Registry)
	Else
		s_source = Replace(T_100b, "%1", m_Doc.Name) ' deriving preferences from selected document
	End If

	' Set up clock code image
	s_picture = MacroDir & "\code13.bmp"

	Begin Dialog UserDialog 460,438,s_source ' %GRID:10,6,1,1
		'PushButton 340,6,100,24,T_10,.ButtonHelp
		OptionGroup .ScheduleFromStart
			OptionButton 30,16,400,12,T_111,.OptionButton6
			OptionButton 30,34,400,12,T_112,.OptionButton7
		CheckBox 30,52,400,18,T_113,.ValidateProject
		CheckBox 30,70,400,18,T_115,.CheckExportable
		Text 30,96,160,18,T_114,.Text1
		DropListBox 210,94,80,78,s_DaysPerWeek(),.WorkingDays
		Text 30,120,160,18,T_114a,.Text6
		DropListBox 210,118,80,60,s_HoursPerDay(),.HoursPerDay
		Text 30,144,330,18,T_116,.Text2
		DropListBox 210,142,80,90,s_DefaultPriority(),.DefaultPriority
		CheckBox 30,168,400,24,T_122,.UseMilestoneCode
		TextBox 30,196,160,24,.DefaultResource
		Text 200,196,230,30,T_117,.Text3
		GroupBox 20,228,420,60,T_123,.GroupBox2
		OptionGroup .ImportSchedule
			OptionButton 30,244,380,12,T_124,.OptionButton1
			OptionButton 30,262,380,12,T_125,.OptionButton2
		GroupBox 20,294,420,102,T_118,.GroupBox1
		OptionGroup .DoRelationships
			OptionButton 30,310,390,12,T_119,.OptionButton3
			OptionButton 30,330,400,12,T_120,.OptionButton4
			OptionButton 30,348,400,12,T_121,.OptionButton5
		Text 30,366,300,18,T_126,.Text4
		DropListBox 340,366,80,72,s_DefaultRelationship(),.DefaultRelationship
		OKButton 200,402,110,24
		PushButton 320,402,110,24,T_130,.PushButton1
	End Dialog
	Dim dlg_Prefs As UserDialog

	' Initialise current preferences in dialogue
	dlg_Prefs.ScheduleFromStart = IIf(b_ScheduleFromStart, 0, 1)
	dlg_Prefs.ValidateProject = IIf(b_ValidateProject, 1, 0)
	dlg_Prefs.CheckExportable = IIf(b_CheckExportable, 1, 0)
	dlg_Prefs.WorkingDays = i_WorkingDaysPerWeek - 1 ' assign array pointers
	dlg_Prefs.HoursPerDay = i_HoursPerDay - 1
	dlg_Prefs.DefaultPriority = i_DefaultPriority - 1
	dlg_Prefs.DefaultRelationship = i_DefaultRelationship
	dlg_Prefs.DefaultResource = Trim(s_DefaultResource)
	dlg_Prefs.DoRelationships = i_DoRelationships
	dlg_Prefs.UseMilestoneCode = IIf(b_UseMilestoneCode, 1, 0)
	dlg_Prefs.ImportSchedule = i_ImportSchedule

	f_Preferences = False ' assume we will close dialogue
	Select Case Dialog(dlg_Prefs)
		Case -1 ' ok button clicked
			' Unload new preferences from dialogue
			b_ScheduleFromStart = (dlg_Prefs.ScheduleFromStart = 0)
			b_ValidateProject = (dlg_Prefs.ValidateProject = 1)
			b_CheckExportable = (dlg_Prefs.CheckExportable = 1)
			i_WorkingDaysPerWeek = dlg_Prefs.WorkingDays + 1 ' working days per week
			i_HoursPerDay = dlg_Prefs.HoursPerDay + 1 ' hours per day
			i_DefaultPriority = dlg_Prefs.DefaultPriority + 1 ' default priority setting
			i_DefaultRelationship = dlg_Prefs.DefaultRelationship ' default task relationship type
			s_DefaultResource = Trim(dlg_Prefs.DefaultResource) ' default resource allocation
			i_DoRelationships = dlg_Prefs.DoRelationships
			b_UseMilestoneCode = (dlg_Prefs.UseMilestoneCode = 1)
			i_ImportSchedule = dlg_Prefs.ImportSchedule
			' Save new settings
			Call p_SavePrefs(m_Doc)

		'Case 1 ' Help button clicked
		'	If Len(s_HelpFile) > 0 Then Call f_OpenDoc(MacroDir & "\" & s_HelpFile)
		'	If Len(s_HelpMap) > 0 Then Documents.Open(MacroDir & "\" & s_HelpMap, "", True).Activate ' open Help map

		Case 1 ' Reset button clicked
			f_Preferences = True ' keep dialogue open

	End Select

End Function

Function f_ImportMap() As Boolean
' Import map at current selection point to the project object
' Returns True if successful, False else
Dim i_1 As Integer

	f_ImportMap = False ' assume an error

	' Check we have a document to export
	If Documents.Count = 0 Then ' no document
		Err.Raise(1000,,T_31,,)
	End If
	Set m_wkgDoc = ActiveDocument
	If m_wkgDoc.Selection.Count = 0 Then m_wkgDoc.CentralTopic.SelectOnly
	Select Case m_wkgDoc.Selection.PrimaryObject.Type
		Case mmDocumentObjectTypeTopic ' confirm any  branch selection
			If Not m_wkgDoc.Selection.PrimaryTopic.IsCentralTopic Then
				Select Case MsgBox(T_19, vbQuestion + vbYesNoCancel, T_0exp)
					Case vbYes
					Case vbNo
						m_wkgDoc.CentralTopic.SelectOnly ' default to map title
					Case Else ' cancel
						Exit Function
				End Select
			End If
		Case Else ' anything else
			m_wkgDoc.CentralTopic.SelectOnly ' default to map title
	End Select
	Set m_wkgTopic = m_wkgDoc.Selection.PrimaryTopic

	' Set up defaults from preferences
	With o_Project1
		.ScheduleFromStart = b_ScheduleFromStart ' Schedule from project start (preference)
		.TimeUnits = 1 ' default to hours units for MPX export
		.HoursPerDay = i_HoursPerDay
		.DaysPerWeek = i_WorkingDaysPerWeek ' working days per week (preference)
	End With

	' Validate project tree if required
	If b_ValidateProject Or b_CheckExportable Then
		If Not f_ValidateBranch(m_wkgTopic, 0, 0, 0) Then Exit Function ' abandon
	End If
	m_wkgTopic.SelectOnly ' reselect project for export again

	' Import tasks here
	Call p_ImportBranch(m_wkgTopic, 0, "") ' import tree under here

	' Fix up task cross-references to use Task IDs instead of branch IDs
	For i_1 = 1 To o_Project1.Tasks.Count
		o_Project1.Tasks.Item(i_1).Predecessors = f_FixTaskRefs(o_Project1.Tasks.Item(i_1).Predecessors)
		o_Project1.Tasks.Item(i_1).Successors = f_FixTaskRefs(o_Project1.Tasks.Item(i_1).Successors)
	Next i_1

	' Fix the default project start/end dates
	Call p_FixProjectDates()

	' Fix up the resource initials required for SureTrak
	Call p_FixResources()

	If b_Debug Then Call p_Dump() ' dump project
	f_ImportMap = True ' success

End Function

Function f_FixTaskRefs(ByVal s_list As String) As String
' Fix up a list of task relationships (predecessors or successors)
' Convert Branch ID references to Task ID References
Dim i_1 As Long
Dim i_2 As Long
Dim s_parts As Variant ' array holder
Dim s_1 As String
Dim s_2 As String
Dim s_3 As String

	f_FixTaskRefs = "" ' assume nothing to return
	s_list = Trim(s_list)
	If Len(s_list) = 0 Then Exit Function ' no list to fix
	s_3 = "" ' initialise new list
	's_parts = Split(s_list, s_MPXsep) ' split list into parts
	s_parts = f_Split(s_list, s_MPXsep) ' split list into parts
	For i_1 = 0 To UBound(s_parts)
		s_1 = s_parts(i_1) ' read id & descriptor
		s_parts(i_1) = "" ' empty it out
		If f_extractID(s_1, i_2, s_2) Then ' extracted an ID and descriptor
			i_2 = f_FindTask(i_2, False) ' convert Branch ID into task ID
			If i_2 > 0 Then ' found OK
				' Manually rebuild list instead of using join in case there are gaps
				If Len(s_3) > 0 Then s_3 = s_3 & s_MPXsep ' append separator
				s_3 = s_3 & CStr(o_Project1.Tasks.Item(i_2).ID) & s_2 ' append task ID and relationship descriptor
			End If
		End If
	Next i_1
	f_FixTaskRefs = s_3 ' return final list

End Function

Sub p_FixResources()
' Fix the Resource initials required for some packages to import resources
' This cannot be done on the fly because duplicated initials may occur
' Initials are the first 3 chars plus a number if duplicates are encountered
Dim i_1 As Integer
Dim i_2 As Integer
Dim i_suffix As Integer
Dim s_1 As String
Dim s_2 As String
Dim b_dup As Boolean

	For i_1 = 1 To o_Project1.Resources.Count
		s_1 = UCase(Trim(o_Project1.Resources.Item(i_1).Name)) ' read resource name
		i_suffix = 1 ' initialise suffix
		If Len(s_1) > 0 Then
			' Derive initials for this resource
			s_1 = Replace(s_1, ".", "") ' strip punctuation and spaces
			s_1 = Replace(s_1, " ", "")
			s_1 = Left(s_1, 3) ' use first 3 letters or less
			' Check whether this has been used already
			b_dup = True ' get into loop
			While b_dup
				b_dup = False
				s_2 = s_1 & IIf(i_suffix > 1, CStr(i_suffix), "")
				For i_2 = 1 To o_Project1.Resources.Count
					If o_Project1.Resources.Item(i_2).Initials = s_2 Then
						b_dup = True
						i_suffix = i_suffix + 1 ' try next suffix
						Exit For
					End If
				Next i_2
			Wend
			' Write final initials back to resource
			o_Project1.Resources.Item(i_1).Initials = s_2
		End If
	Next i_1

End Sub

Sub p_FixProjectDates()
' Fix the Project Start and Finish dates, provide defaults if necessary
Dim i_1 As Integer
Dim d_1 As Date
Dim d_2 As Date

	If o_Project1.ScheduleFromStart And (o_Project1.StartDate = 0) Then
		' scheduling from start but no start date was defined : find earliest start date
		d_1 = 0 ' assume no start date
		For i_1 = 1 To o_Project1.Tasks.Count
			d_2 = o_Project1.Tasks.Item(i_1).StartDate ' read task start date
			If d_2 <> 0 Then ' has a start date
				If (d_1 = 0) Or (d_2 < d_1) Then d_1 = d_2 ' use this one
			End If
		Next i_1
		If d_1 = 0 Then d_1 = Date() ' default to now if still undefined
		o_Project1.StartDate = d_1
	End If
	If (Not o_Project1.ScheduleFromStart)  And (o_Project1.FinishDate = 0) Then
		' scheduling from finish but no finish date was defined: find latest due date
		d_1 = 0 ' assume no finish date
		For i_1 = 1 To o_Project1.Tasks.Count
			d_2 = o_Project1.Tasks.Item(i_1).DeadlineDate ' read task due date
			If d_2 <> 0 Then ' has a due date
				If (d_1 = 0) Or (d_2 > d_1) Then d_1 = d_2 ' use this one
			End If
		Next i_1
		If d_1 = 0 Then d_1 = Date() ' default to now if still undefined after this
		o_Project1.FinishDate = d_1
	End If

End Sub

Function f_ExportMap2() As Boolean
' Export the project object to a map using the Transaction object if required

	' Set up map to import to
	If Documents.Count = 0 Then ' create a new map
		Set m_wkgDoc = Documents.Add(True) ' add blank document
		m_wkgDoc.Activate
		Set m_wkgTopic = Nothing ' no working branch, start at map title
	Else ' use current document
		Set m_wkgDoc = ActiveDocument
		' Check if there is an import point selected
		If m_wkgDoc.Selection.PrimaryTopic Is Nothing Then m_wkgDoc.CentralTopic.SelectOnly
		If m_wkgDoc.Selection.PrimaryObject.Type <> mmDocumentObjectTypeTopic Then ' [1-000396] ".SelectedObject" : See "How to select objects"
			m_wkgDoc.CentralTopic.SelectOnly ' default to the map title
		End If
		Set m_wkgTopic = m_wkgDoc.Selection.PrimaryTopic ' [1-000398] ".SelectedBranch" : See "How to select objects"
	End If

	If b_UseTransaction Then
		' Set up a transaction object for this map
		Set mTrans = m_wkgDoc.NewTransaction(s_AppName)
		' Kick off transaction
		If Not mTrans Is Nothing Then
			Debug.Print("Starting m_trans")
			mTrans.Start
			Wait(1.0)
			' Wait for it to finish
			While mTrans.IsExecuting
				Wait (0.1)
			Wend
		End If
	Else ' normal export
		Call f_ExportMap()
	End If
	' Finished
	f_ExportMap2 = True

End Function

Private Sub mTrans_Execute(m_Doc As Document)
' Export to the transaction object then update it

	Debug.Print("mTrans_Execute()")
	Call f_ExportMap() ' export current map
	Debug.Print("mTrans.Update")
	mTrans.Commit

End Sub

Function f_ExportMap() As Boolean
' Export the project object to a map
' Returns True if successful, false else
Dim m_Insert As Topic ' [1-003372] "As Branch" : Branch becomes Topic
Dim i_1 As Long
Dim i_2 As Long
Dim i_3 As Long
Dim i_4 As Long
Dim i_5 As Long
Dim i_FirstTask As Integer
Dim i_OutlineBase As Integer ' Outline level base
Dim i_level As Integer
Dim s_sep As String
Dim s_1 As String
Dim s_parts As Variant ' array for list splitting
Dim r_1 As Relationship
Dim m_b1 As Topic ' [1-003372] "As Branch" : Branch becomes Topic
Dim m_b2 As Topic ' [1-003372] "As Branch" : Branch becomes Topic

	' Insert project root branch
	If m_wkgTopic Is Nothing Then
		Set m_wkgTopic = m_wkgDoc.CentralTopic ' start at map title
		Set m_Insert = m_wkgDoc.CentralTopic ' insert point
	End If
 	Set m_Insert = m_wkgTopic.AddSubTopic(T_4)
	' Check if we have a project root task in the task list
	If o_Project1.Tasks.Count > 0 Then
		If o_Project1.Tasks.Item(1).ID = 0 Then ' this is the project root branch
			i_FirstTask = 2 ' first actual task starts at 2, skip #1
		Else ' no project root branch available: use information from Project Record
			i_FirstTask = 1 ' first actual task starts at 1
		End If
	End If
	m_Insert.Text = o_Project1.Name
	m_Insert.Notes.Text = Replace(Replace(T_16, "%2", o_Project1.FileName), "%1", Format(Now())) & _
			IIf((i_ImportSchedule = 0), T_17, T_18) & _
			o_Project1.Notes
	m_Insert.CreateHyperlink(o_Project1.FileName) ' insert mpx filename as hyperlink
	If o_Project1.StartDate <> 0 Then m_Insert.Task.StartDate = o_Project1.StartDate
	If o_Project1.FinishDate <> 0 Then m_Insert.Task.DueDate = o_Project1.FinishDate ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"
	'm_Insert.Duration = o_Project1.Duration ' Project Duration
	'm_Insert.PercentComplete = o_Project1.PercentComplete ' Project Percent complete

	' Normalise the Outline Level
	' Outline levels may be base 0 or 1 - check it by finding the lowest
	i_OutlineBase = 999 ' arbitrarily high number
	For i_1 = 1 To o_Project1.Tasks.Count ' check for lowest outline level throughout
		If o_Project1.Tasks.Item(i_1).OutlineLevel < i_OutlineBase Then i_OutlineBase = o_Project1.Tasks.Item(i_1).OutlineLevel
	Next i_1
	If i_OutlineBase = 999 Then i_OutlineBase = 0 ' no outline level information found

	' Add task branches
	i_level = i_FirstTask - 1 ' initialise working level; 1 if Project Root branch existed
	For i_1 = i_FirstTask To o_Project1.Tasks.Count
		With o_Project1.Tasks.Item(i_1)
			' Find right level
			While i_level > (.OutlineLevel - i_OutlineBase) ' while too deep
				Set m_Insert = m_Insert.ParentTopic ' go back up
				i_level = i_level - 1
			Wend
			While i_level < (.OutlineLevel - i_OutlineBase) ' while too high
				Set m_Insert = m_Insert.AddSubTopic(T_4)
				i_level = i_level + 1
			Wend
			' Now add new branch at this level
			Set m_Insert = m_Insert.AddSubTopic(T_4)
 			.BranchGuid = m_Insert.Guid ' save original Guid
			.BranchID = f_HashGuid(m_Insert.Guid) ' save the branch ID of this branch
			If Len(.Name) > 0 Then m_Insert.Text = .Name
			i_level = i_level + 1 ' move to this level
			If Len(.Notes) > 0 Then m_Insert.Notes.Text = .Notes
			If .StartDate <> 0 Then m_Insert.Task.StartDate = .StartDate ' [1-001002] ".StartDate" : See "How to find all branch properties"
			If .DeadlineDate <> 0 Then m_Insert.Task.DueDate = .DeadlineDate ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"
			'If .Duration > 0 Then m_Insert.Task.Duration = .Duration ' only write if non-zero
			m_Insert.Task.DurationInHours = .Duration ' write duration for all branches
			If b_UseMilestoneCode Then ' we are using milestone codes
				If .IsMilestone Then m_Insert.Icons(True).AddStockIcon(mmStockIconClock) ' write Milestone code to branch
				' Any branch with children cannot be a milestone task, so remove any milestone code on parent
				If m_Insert.ParentTopic.Icons(True).HasStockIcon(mmStockIconClock) Then
					m_Insert.ParentTopic.Icons(True).RemoveStockIcon(mmStockIconClock)
				End If
			End If
			If (.Priority > 0) And (.Priority <> i_DefaultPriority) Then
				' Convert priority settings and apply to branch
				m_Insert.Task.Priority = .Priority ' use raw priority
			End If
			' Any branch with children cannot have a task priority, so disallow priority on parent
			'If Not (m_Insert.ParentBranch.IsTitle) Then m_Insert.ParentBranch.Priority = 0 ' none
			If .PercentComplete > 0 Then m_Insert.Task.Complete = .PercentComplete ' [1-001058] ".PercentageDone" : See "How to find all branch properties"
			' Touch Percent Complete if a duration is specified or if a milestone
			If (.Duration > 0) Or (b_UseMilestoneCode And .IsMilestone) Then
				m_Insert.Task.Complete = .PercentComplete ' touch % Done to make sure Task marker comes up
			End If
			' Add Resources
			s_sep = "" ' no separator
			For i_2 = 1 To o_Project1.Assignments.Count
				If o_Project1.Assignments.Item(i_2).TaskID = .ID Then
					' Fetch resource name
					i_3 = o_Project1.Resources.FindbyID(o_Project1.Assignments.Item(i_2).ResourceID) ' look up its index
					s_1 = o_Project1.Resources.Item(i_3).Name
					' Check it does not include the resource separator that MM uses
					If InStr(s_1, s_ResourceSep) > 0 Then s_1 = Replace(s_1, s_ResourceSep, "") ' strip it
					m_Insert.Task.Resources = m_Insert.Task.Resources & s_sep & s_1 ' [1-001110] ".Resources" : See "How to find all branch properties"
					s_sep = s_ResourceSep & " " ' update separator for next resource
				End If
			Next i_2
		End With
	Next i_1

	' Import Task Predecessors (1) or Successors (2) using Relationships
	If i_DoRelationships > 0 Then
		For i_5 = 1 To 2 ' do both predecessors and successors
			For i_1 = i_FirstTask To o_Project1.Tasks.Count
				With o_Project1.Tasks.Item(i_1)
					s_1 = IIf(i_5 = 1, Trim(.Predecessors), Trim(.Successors))
					If Len(s_1) > 0 Then ' there are some linked tasks defined
						's_parts = Split(s_1, s_MPXsep) ' split up list
						s_parts = f_Split(s_1, s_MPXsep) ' split up list
						For i_2 = 0 To UBound(s_parts)
							' Decode remote task
							If f_extractID(s_parts(i_2), i_3, s_1) Then ' successfully extracted ID
								i_4 = f_FindTask(i_3, True) ' find the task with this Task ID
								If i_4 > 0 Then ' found it OK
									' Create a relationship from this branch
									Set m_b1 = m_wkgDoc.Range(mmRangeAllTopics, False).FindByGuid(.BranchGuid) ' here
									Set m_b2 = m_wkgDoc.Range(mmRangeAllTopics, False).FindByGuid(o_Project1.Tasks.Item(i_4).BranchGuid) ' remote branch
									Set r_1 = Nothing
									If i_5 = 1 Then ' create predecessor link unless already exists
										If Not f_IsLinked(m_b2, m_b1) Then Set r_1 = m_b2.Relationships(True).AddToTopic(m_b1, "")
									Else ' create successor link unless already existing
										If Not f_IsLinked(m_b1, m_b2) Then Set r_1 = m_b1.Relationships(True).AddToTopic(m_b2, "")
									End If
									If Not (r_1 Is Nothing) Then ' a new relationship was added
										r_1.Shape1 = mmConnectionShapeNoArrow
										r_1.Shape2 = mmConnectionShapeArrow
										If Len(s_1) > 0 Then ' this is a typed relationship
											If s_1 <> f_ConvertTRD(i_DefaultRelationship) Then
												If r_1.CalloutTopics(True).Count = 0 Then r_1.CalloutTopics(True).Add("")
												r_1.CalloutTopics(True).Item(1).Text = s_1 ' assign it if not the default
											Else ' clear any default text
												If r_1.CalloutTopics(True).Count > 0 Then r_1.CalloutTopics(True).Item(1).Delete
											End If
										End If
									End If
								End If
							End If
						Next i_2
					End If
				End With
			Next i_1
		Next i_5
		Set r_1 = Nothing
	End If

	f_ExportMap = True ' finished OK

End Function

Function f_extractID(ByVal s_combo As String, ByRef i_ID As Long, ByRef s_Type As String) As Boolean
' Extract the ID and relationship type from a combined successor or predecessor descriptor (e.g. "5SS")
' Return values written into byref variables
' Return True if read successfully, false else
Dim i_1 As Integer

	i_ID = 0 ' assume no value
	s_Type = ""
	f_extractID = False
	s_combo = Trim(s_combo)
	If Len(s_combo) = 0 Then Exit Function ' nothing to read
	i_1 = 1 ' parse number
	While IsNumeric(Mid(s_combo, i_1, 1)) And i_1 <= Len(s_combo)
		i_1 = i_1 + 1
	Wend
	' extract ID
	If i_1 > 1 Then ' there is an ID
		i_ID = Val(Left(s_combo, (i_1 - 1)))
		' extract descriptor too
		s_Type = "" ' assume none
		If i_1 <= Len(s_combo) Then s_Type = Right(s_combo, (Len(s_combo) - i_1) + 1) ' there is a descriptor
		s_Type = f_ValidateTRD(s_Type)
		f_extractID = True ' completed OK
	End If

End Function

Function f_ValidateTRD(ByVal s_descriptor As String) As String
' Validate task relationship descriptor, return sanitised version

	f_ValidateTRD = f_ConvertTRD(f_ConvertTRD(s_descriptor))

End Function

Function f_ConvertTRD(ByVal v_input As Variant) As Variant
' Forward and backwards lookup of task relationship descriptor
' The relationship between numerical and text types is kept in one routine for clarity

	If IsNumeric(v_input) Then
		Select Case CInt(v_input)
			Case 0
				f_ConvertTRD = T_381 ' Start to Start
			Case 1
				f_ConvertTRD = T_382 ' Start to Finish
			Case 2
				f_ConvertTRD = T_383 ' Finish to Start
			Case 3
				f_ConvertTRD = T_384 ' Finish to Finish
			Case Else
				f_ConvertTRD = f_ConvertTRD(i_DefaultRelationship)
		End Select
	Else ' string to numeric conversion
		Select Case UCase(Trim(v_input))
			Case T_381 ' Start to Start
				f_ConvertTRD = 0
			Case T_382 ' Start to Finish
				f_ConvertTRD = 1
			Case T_383 ' Finish to Start
				f_ConvertTRD = 2
			Case T_384 ' Finish to Finish
				f_ConvertTRD = 3
			Case Else ' Anything else
				f_ConvertTRD = i_DefaultRelationship
		End Select
	End If

End Function

Function f_FindTask(ByVal v_ID As Variant, ByVal b_IsTaskID As Boolean) As Integer
' Locate a task with a given ID
' If b_IsTaskID true then search for task by task ID, else search by Branch ID
' Return the index number of the matching task
' Return -1 if not found
Dim i_1 As Integer

	f_FindTask = -1 ' default to not found
	If b_IsTaskID Then ' search by task ID
		For i_1 = 1 To o_Project1.Tasks.Count
			If o_Project1.Tasks.Item(i_1).ID = v_ID Then
				f_FindTask = i_1
				Exit Function
			End If
		Next i_1
	Else ' search by branch ID (hashed Guid)
		For i_1 = 1 To o_Project1.Tasks.Count
			If o_Project1.Tasks.Item(i_1).BranchID = v_ID Then
				f_FindTask = i_1
				Exit Function
			End If
		Next i_1
	End If

End Function

 ' Function f_IsLinked(ByVal m_b1 As Branch, ByVal m_b2 As Branch) As Integer
Function f_IsLinked(ByVal m_b1 As Topic, ByVal m_b2 As Topic) As Integer ' [1-003372] "As Branch" : Branch becomes Topic
' Check whether first branch is linked to second branch with a relationship
' Return the number of connections
Dim r_1 As Relationship
Dim i_1 As Integer
Dim v_1 As MmConnectionShape
Dim v_2 As MmConnectionShape
Dim m_b3 As Topic ' [1-003372] "As Branch" : Branch becomes Topic
Dim i_Count As Integer ' number of connections found

	i_Count = 0
	For i_1 = 1 To m_b1.Relationships(False).Count ' Check all relationships on first branch
		Set r_1 = m_b1.Relationships(False).Item(i_1) ' get relationship
		v_1  = mmConnectionShapeNoArrow ' assume no arrows
		v_2  = mmConnectionShapeNoArrow
		Set m_b3 = Nothing
		' Work out which way round it is
		If r_1.ConnectedObject1 Is m_b1 Then ' end 1 is us
			' Check it points at another topic
			If r_1.ConnectedObject2.Type = mmDocumentObjectTypeTopic Then
				v_1 = r_1.Shape1 ' local arrowhead
				v_2 = r_1.Shape2 ' remote arrowhead
				Set m_b3 = r_1.ConnectedObject2 ' remote end
			End If
		Else ' end 2 is us
			' Check it points at another topic
			If r_1.ConnectedObject1.Type = mmDocumentObjectTypeTopic Then
				v_1 = r_1.Shape2 ' local arrowhead
				v_2 = r_1.Shape1 ' remote arrowhead
				Set m_b3 = r_1.ConnectedObject1 ' remote end
			End If
		End If
		If v_2 <> mmConnectionShapeNoArrow  Then ' arrow points from m_b1 to m_b3
			If (m_b3 Is m_b2) Then i_Count = i_Count + 1 ' this is a relationship from m_b1 to m_b2
		End If
	Next i_1
	' Finished, clean up
	Set m_b3 = Nothing
	Set r_1 = Nothing
	f_IsLinked = i_Count ' return number of matching relationships found

End Function

Function f_OtherEnd(ByVal m_bHere As Topic, ByVal r_1 As Relationship) As Topic ' [1-003372] "As Branch" : Branch becomes Topic
' Get the branch object at the other end of a relationship

	If (r_1.ConnectedObject1 Is m_bHere) Then ' [1-001646] ".Branch1" : A Relationship can now be connected to a Topic or a Boundary.; See "How to get an object's type"
		Set f_OtherEnd = r_1.ConnectedObject2 ' [1-001648] ".Branch2" : A Relationship can now be connected to a Topic or a Boundary.; See "How to get an object's type"
	Else
		Set f_OtherEnd = r_1.ConnectedObject1 ' [1-001646] ".Branch1" : A Relationship can now be connected to a Topic or a Boundary.; See "How to get an object's type"
	End If

End Function

Function f_IsTwoWay(ByVal r_1 As Relationship) As Boolean
' Check whether a relationship is 2-way

	f_IsTwoWay = ((r_1.Shape1 <> mmConnectionShapeNoArrow) And (r_1.Shape2 <> mmConnectionShapeNoArrow))' arrowheads both ends

End Function

 ' Function f_IsInTree(ByVal m_b1 As Branch, ByVal m_bRoot As Branch) As Boolean
Function f_IsInTree(ByVal m_b1 As Topic, ByVal m_bRoot As Topic) As Boolean ' [1-003372] "As Branch" : Branch becomes Topic
' Check whether branch m_b1 lies in the tree under m_bRoot

	f_IsInTree = True ' assume ok
	While Not (m_b1.IsCentralTopic) ' check up to the title
		If m_b1 Is m_bRoot Then Exit Function
		Set m_b1 = m_b1.ParentTopic ' else go up
	Wend
	' Reached the title
	f_IsInTree = (m_b1 Is m_bRoot) ' last test at root level

End Function

Function f_ImportMPX() As Boolean
' Import a MPX file to the project object
' Returns True if successful, false else
Dim s_MPXfile As String ' MPX filename

	f_ImportMPX = False ' assume cancellation or failure
	' First select the MPX file to import
	s_MPXfile = GetSetting(s_AppName, s_KeySection, s_KeyMRU, "") ' is there a MRU filename?
	If Len(s_MPXfile) = 0 Then s_MPXfile = f_DefaultPath() ' no, get default path

	' Prepare file dialog
	Dim fd As FileDialog
	Set fd = FileDialog(mmFileDialogFilePicker)
	fd.Reset

	' Remove all existing filters
	Dim filterCount As Long
	Dim i_filter As Long
	filterCount = fd.Filters.Count
	For i_filter = 0 To filterCount - 1
		fd.Filters.Delete 1
	Next i_filter

	' Prepare file dialog options
	fd.AllowMultiSelect = False
	fd.PreviewPane = False
	fd.ButtonName = T_51
	fd.Filters.Add(T_60, s_MPXExtLong)
	fd.FilterIndex = fd.Filters.Count
	fd.InitialFileName = s_MPXfile
	fd.Title = T_50
	If fd.Show Then
	    s_MPXfile = fd.Item
	Else
	    s_MPXfile = ""
	End If

	's_MPXfile = GetFilePath(f_cropFolder(s_MPXfile), s_MPXExt, f_cropFile(s_MPXfile), T_50, 0) ' "Select MPX file to import"
	If Len(s_MPXfile) = 0 Then Exit Function ' user cancelled
	SaveSetting(s_AppName, s_KeySection, s_KeyMRU, s_MPXfile) ' save this filename as MRU file
	' Initialise defaults in the Project object
	With o_Project1
		.Name = T_4 ' Untitled
		.ScheduleFromStart = b_ScheduleFromStart ' default to schedule from project start
		.TimeUnits = 1 ' default to hours units unless otherwise declared
		.HoursPerDay = 8 ' hours a day
		.DaysPerWeek = i_WorkingDaysPerWeek ' working days per week
		.StartDate = 0 ' no start date
		.FinishDate = 0 ' no end date
		.MPXMagic = False ' no magic record found (CR 10519)
	End With

	Call p_ImportMPXfile(s_MPXfile) ' import MPX file
	' Apply post filtering to imported data
	With o_Project1
		.FileName = s_MPXfile ' save full filename
		If i_ImportSchedule = 0 Then ' only importing the constraint data
			If .ScheduleFromStart Then ' the finish date is not a constraint
				.FinishDate = 0 ' kill project end date
			Else ' the start date is not a constraint
				.StartDate = 0 ' kill project start date
			End If
		End If
	End With
	If b_Debug Then Call p_Dump() ' Dump newly imported MPX file
	f_ImportMPX = True ' completed OK

End Function

Sub p_ImportMPXfile(ByVal s_Filename As String)
' Import MPX file into the project model
Dim s_line As String
Dim i_1 As Integer

	' Initialise the field positions for the data that MindManager uses
	i_FieldPos_ResourceName = 0
	i_FieldPos_ResourceID = 0
	i_FieldPos_TaskID = 0
	i_FieldPos_TaskName = 0
	i_FieldPos_TaskOutlineLevel = 0
 	i_FieldPos_TaskPriority = 0
 	i_FieldPos_TaskPercentComplete = 0
	i_FieldPos_TaskPercentWorkComplete = 0
	i_FieldPos_TaskStartDate = 0
	i_FieldPos_TaskFinishDate = 0
	i_FieldPos_TaskEarlyStartDate = 0
	i_FieldPos_TaskEarlyFinishDate = 0
	i_FieldPos_TaskLateStartDate = 0
	i_FieldPos_TaskLateFinishDate = 0
 	i_FieldPos_TaskDuration = 0
 	i_FieldPos_TaskMilestone = 0
 	i_FieldPos_TaskActualDuration = 0
 	i_FieldPos_TaskConstraintDate = 0
 	i_FieldPos_TaskConstraintType = 0
 	i_FieldPos_TaskPredecessors = 0
 	i_FieldPos_TaskSuccessors = 0

	' Read and process the file
	i_1 = 0
	Open s_Filename For Input As #1
	While Not EOF(1)
		Line Input #1, s_line
		i_1 = i_1 + 1 ' count line
		s_line = Trim(s_line)
		If Len(s_line) > 0 Then Call p_parseline(s_line, i_1)
		' CR 10519: detect invalid incoming file
		If (i_1 > 5) And (Not o_Project1.MPXMagic) Then ' MPX magic has not been found in first 5 lines, abandon
			Err.Raise(1000,,T_32,,) ' abort with error
		End If
	Wend
	Close #1
	If Not o_Project1.MPXMagic Then ' MPX file might be shorter than 5 lines
		Err.Raise(1000,,T_32,,) ' abort with error
	End If

End Sub

Sub p_parseline(ByRef s_line As String, i_LineNo As Integer)
' Parse a non-zero MPX file line into fields and process them
Dim s_Fields() As String ' Array of fields: entry 0 is line number
Dim i_FieldCount As Integer ' number of fields
Dim i_State As Integer ' state machine state
Dim i_Ptr As Long ' line pointer
Dim i_FieldFirst As Long ' Field character pointers
Dim i_FieldLast As Long
Dim b_Handled As Boolean ' character-handled flag
Dim s_Field As String ' temporary field holder
Dim b_Trim As Boolean ' field is to be trimmed
Dim i_1 As Long

	Erase s_Fields
	ReDim s_Fields(0 To i_FieldCount)
	s_Fields(0) = CStr(i_LineNo) ' store line number for error diagnostics

	i_Ptr = 0 ' will start at first character
	s_line = s_line & s_MPXsep ' ensure Line is terminated
	i_State = IIf(o_Project1.IsValid, 100, 10) ' select first state

	While i_Ptr <= Len(s_line)
		b_Handled = False ' character not handled yet
		i_Ptr = i_Ptr + 1 ' next character
		While Not b_Handled
			Select Case i_State ' select state

				Case 10 ' looking for the "MPX" Magic field
					If UCase(Mid(s_line, i_Ptr, Len(mpx_Magic))) = UCase(mpx_Magic) Then
						i_Ptr = i_Ptr + (Len(mpx_Magic) - 1) ' advance pointer to end of magic string
						i_State = 20 ' look for separator character
						o_Project1.MPXMagic = True ' read magic string
						b_Handled = True
					Else ' not found yet
						b_Handled = True ' try again
					End If

				Case 20 ' read the separator character
					o_Project1.IsValid = True ' have read the File Creation record now
					s_MPXsep = Mid(s_line, i_Ptr, 1) ' read separator char used in file
					If Trim(s_MPXsep) = "" Or IsNumeric(s_MPXsep) Or f_IsAlpha(s_MPXsep) Then
						Err.Raise(1000,,Replace(T_26, "%1", s_MPXsep),,) ' Abandon if invalid separator char
					End If
					i_FieldCount = i_FieldCount + 1 ' create a record number for the file creation record
					ReDim Preserve s_Fields(0 To i_FieldCount)
					s_Fields(i_FieldCount) = CStr(-1) ' internally the File Creation record is numbered -1
					i_State = 100 ' read remainder of fields in the file creation record
					b_Handled = True

				Case 100 ' initialise for new field
					i_FieldFirst = i_Ptr
					i_FieldLast = i_Ptr - 1
					b_Trim = True ' assume not a quoted field
					i_State = 120 ' look for first character

				Case 120 ' first character of field
					Select Case Mid(s_line, i_Ptr, 1)
						Case s_MPXsep ' empty field
							i_State = 190 ' add it to list
						Case mpx_Cquote ' start of quoted field
							i_State = 140 ' read quoted field
							i_FieldFirst = i_Ptr + 1 ' field starts on next char
							b_Trim = False ' quoted field, not trimmed
							b_Handled = True
						Case " " ' white space
							b_Handled = True ' ignore leading unquoted white space chars
						Case vbTab ' white space
							b_Handled = True ' ignore leading unquoted white space chars
						Case Else ' a real character, field starts
							i_FieldFirst = i_Ptr ' field starts here
							i_State = 130 ' read normal field
					End Select

				Case 130 ' read normal field
					' Slow version reads every char
					Select Case Mid(s_line, i_Ptr, 1)
						Case s_MPXsep ' field ends
							i_State = 190
						Case Else ' else a field character
							i_FieldLast = i_Ptr ' move last-char pointer
							b_Handled = True
					End Select

				Case 140 ' read quoted field
					Select Case Mid(s_line, i_Ptr, 1)
						Case mpx_Cquote ' provisional quote char
							i_State = 141
							b_Handled = True
						Case Else ' other characters
							i_FieldLast = i_Ptr ' move last-char pointer
							b_Handled = True
					End Select

				Case 141 ' provisional quote character received
					Select Case Mid(s_line, i_Ptr, 1)
						Case mpx_Cquote ' second quote char
							i_FieldLast = i_Ptr ' count these chars
							i_State = 140 ' go back to reading quoted field chars
							b_Handled = True
						Case Else ' other characters
							i_State = 130 ' quoting finished, treat as normal field
					End Select

				Case 190 ' Field ended
					i_FieldCount = i_FieldCount + 1
					ReDim Preserve s_Fields(0 To i_FieldCount)
					If i_FieldLast >= i_FieldFirst Then
						s_Field = Mid(s_line, i_FieldFirst, (i_FieldLast - i_FieldFirst) + 1)
						s_Field = Replace(s_Field, mpx_Cquote & mpx_Cquote, mpx_Cquote) ' escape double quotes
						s_Field = Replace(s_Field, mpx_Cnewline, vbCrLf) ' escape newline chars
						If b_Trim Then s_Field = Trim(s_Field) ' trim field if it was not quoted
						s_Fields(i_FieldCount) = s_Field ' store in fields array
					Else ' no data
						s_Fields(i_FieldCount) = ""
					End If
					b_Handled = True ' character dealt with
					i_State = 100 ' initialise for new field
			End Select
		Wend
	Wend

	' Parse the fields in the file; the record number must exist at least
	If i_FieldCount >= 1 Then Call p_ParseMPXRecord(i_FieldCount, s_Fields())

End Sub

Sub p_ParseMPXRecord(ByVal i_FieldCount As Integer, ByRef s_Fields() As String)
' Parse a MPX Record comprising a set of fields
' Field(0) is the line number from the file
' Field(1) is the record number
Dim s_1 As String
Dim i_1 As Long
Dim i_2 As Long

	If b_Debug Then
		s_1 = "Parsing Record: "
		For i_1 = 1 To i_FieldCount
			s_1 = s_1 & s_Fields(i_1) & s_MPXsep
		Next i_1
		Debug.Print s_1
	End If

	' Validate record number
	s_1 = Trim(s_Fields(1)) ' read record number
	If Not IsNumeric(s_1) Then Err.Raise(1000,,Replace(Replace(T_27, "%1", s_1), "%2", s_Fields(0)),,)
	Select Case CInt(s_1)
		Case -1 ' file creation record

		Case mpx_RN_Currency ' (10) currency settings
			If i_FieldCount >= 6 Then s_DecimalSep = s_Fields(6) ' [6] Decimal Separator

		Case mpx_RN_Default ' (11) default settings
			With o_Project1
				If i_FieldCount >= 2 Then If Len(s_Fields(2)) > 0 Then .TimeUnits = CInt(s_Fields(2)) ' [2] Default time units
				If i_FieldCount >= 5 Then If Len(s_Fields(5)) > 0 Then .HoursPerDay = CInt(s_Fields(5)) ' [5] Hours per day
				If (i_FieldCount >= 6) And (.HoursPerDay > 0) Then If Len(s_Fields(6)) > 0 Then _
					.DaysPerWeek = CInt(s_Fields(6))/.HoursPerDay ' Days per week = [6] Hours per week / [5] Hours per day
			End With

		Case mpx_RN_DateTime ' (12) Date and time settings
			If i_FieldCount >= 2 Then If Len(s_Fields(2)) > 0 Then i_DateOrder = CInt(s_Fields(2)) ' [2] Date field order
			If i_FieldCount >= 5 Then s_DateSep = s_Fields(5) ' [5] Date separator
			'If i_FieldCount >= 9 Then If Len(s_Fields(9)) > 0 Then i_DateFormat = CInt(s_Fields(9)) ' [9] Date format
			If i_FieldCount >= 9 Then i_DateFormat = CInt(s_Fields(9)) ' [9] Date format

		Case mpx_RN_BaseCalDef ' (20) Base calendar definition

		Case mpx_RN_BaseCalHours ' (25) Base calendar hours

		Case mpx_RN_BaseCalExcep ' (26) Base calendar exceptions

		Case mpx_RN_ProjectHeader ' (30) Project header record
			With o_Project1
				If i_FieldCount >= 2 Then .Name = s_Fields(2) ' [2] Project tab
				If i_FieldCount >= 6 Then .StartDate = f_MPXToDate(s_Fields(6)) ' [6] Start date
				If i_FieldCount >= 7 Then .FinishDate = f_MPXToDate(s_Fields(7)) ' [7] Finish date
				If i_FieldCount >= 8 Then If Len(s_Fields(8)) > 0 Then .ScheduleFromStart = (CInt(s_Fields(8)) = 0) ' [8] Schedule from start
				If i_FieldCount >= 10 Then .Notes = s_Fields(10) ' Project notes taken from [10] Comments
			End With

		Case mpx_RN_TextResources ' (40) Text version of resources table definition
			i_FieldPos_ResourceName = f_FindField(i_FieldCount, s_Fields(), T_300) ' Look for Name
			i_FieldPos_ResourceID = f_FindField(i_FieldCount, s_Fields(), T_301) ' Look for ID

		Case mpx_RN_NumericResources ' (41) Numeric version of resources table definition
			i_FieldPos_ResourceName = f_FindField(i_FieldCount, s_Fields(), 1) ' Look for (1) Name
			i_FieldPos_ResourceID = f_FindField(i_FieldCount, s_Fields(), 40) ' Look for (40) ID

		Case mpx_RN_Resource ' (50) Resource record
			o_Project1.Resources.Add(T_4) ' add an untitled resource
			With o_Project1.Resources.Item(o_Project1.Resources.Count)
				If (i_FieldPos_ResourceName > 0) And (i_FieldCount >= i_FieldPos_ResourceName) Then _
					.Name = s_Fields(i_FieldPos_ResourceName)
				If (i_FieldPos_ResourceID > 0) And (i_FieldCount >= i_FieldPos_ResourceID) Then _
					.ID = CLng(Trim(s_Fields(i_FieldPos_ResourceID)))
			End With

		Case mpx_RN_ResourceNotes ' (51) Notes for a resource record
			If o_Project1.Resources.Count > 0 Then ' append to newest Resource
				If (i_FieldCount >= 2) Then _
					o_Project1.Resources.Item(o_Project1.Resources.Count).Notes = s_Fields(2) ' [2] Notes
			End If

		Case mpx_RN_ResourceCalDef ' (55) Resource calendar definition

		Case mpx_RN_ResourceCalHours ' (56) Resource calendar hours

		Case mpx_RN_ResourceCalExcep ' (57) Resource Calendar Exceptions

		Case mpx_RN_TextTaskTableDef ' (60) Text Task Table definitions
			i_FieldPos_TaskName = f_FindField(i_FieldCount, s_Fields(), T_310) ' Name
			i_FieldPos_TaskID = f_FindField(i_FieldCount, s_Fields(), T_311) ' ID
			i_FieldPos_TaskOutlineLevel = f_FindField(i_FieldCount, s_Fields(), T_312) ' Outline level
		 	i_FieldPos_TaskPriority = f_FindField(i_FieldCount, s_Fields(), T_313) ' Priority
		 	i_FieldPos_TaskPercentComplete = f_FindField(i_FieldCount, s_Fields(), T_314) ' % Complete
			i_FieldPos_TaskPercentWorkComplete = f_FindField(i_FieldCount, s_Fields(), T_315) ' % Work Complete
			i_FieldPos_TaskStartDate = f_FindField(i_FieldCount, s_Fields(), T_316a) ' Task Start Date
			i_FieldPos_TaskFinishDate = f_FindField(i_FieldCount, s_Fields(), T_316b) ' Task Finish Date
			i_FieldPos_TaskEarlyStartDate = f_FindField(i_FieldCount, s_Fields(), T_316c) ' Task Early Start Date
			i_FieldPos_TaskEarlyFinishDate = f_FindField(i_FieldCount, s_Fields(), T_316d) ' Task Early Finish Date
			i_FieldPos_TaskLateStartDate = f_FindField(i_FieldCount, s_Fields(), T_316e) ' Task Late Start Date
			i_FieldPos_TaskLateFinishDate = f_FindField(i_FieldCount, s_Fields(), T_316f) ' Task Late Finish Date
		 	i_FieldPos_TaskDuration = f_FindField(i_FieldCount, s_Fields(), T_317) ' Duration
		 	i_FieldPos_TaskActualDuration = f_FindField(i_FieldCount, s_Fields(), T_318) ' Actual Duration
		 	i_FieldPos_TaskConstraintDate = f_FindField(i_FieldCount, s_Fields(), T_319) ' Constraint Date
		 	i_FieldPos_TaskConstraintType = f_FindField(i_FieldCount, s_Fields(), T_320) ' Constraint Type
		 	i_FieldPos_TaskPredecessors = f_FindField(i_FieldCount, s_Fields(), T_324) ' Task Predecessors
			i_FieldPos_TaskSuccessors = f_FindField(i_FieldCount, s_Fields(), T_325) ' Task Successors
			i_FieldPos_TaskMilestone = f_FindField(i_FieldCount, s_Fields(), T_326) ' Milestone

		Case mpx_RN_NumericTaskTableDef ' (61) Numeric Task Table definitions
			i_FieldPos_TaskID = f_FindField(i_FieldCount, s_Fields(), 90) ' ID
			i_FieldPos_TaskName = f_FindField(i_FieldCount, s_Fields(), 1) ' Name
			i_FieldPos_TaskOutlineLevel = f_FindField(i_FieldCount, s_Fields(), 3) ' Outline level
		 	i_FieldPos_TaskPriority = f_FindField(i_FieldCount, s_Fields(), 95) ' Priority
		 	i_FieldPos_TaskPercentComplete = f_FindField(i_FieldCount, s_Fields(), 44) ' % Complete
			i_FieldPos_TaskPercentWorkComplete = f_FindField(i_FieldCount, s_Fields(), 25) ' % Work Complete
			i_FieldPos_TaskStartDate = f_FindField(i_FieldCount, s_Fields(), 50) ' Start
			i_FieldPos_TaskFinishDate = f_FindField(i_FieldCount, s_Fields(), 51) ' Finish
			i_FieldPos_TaskEarlyStartDate = f_FindField(i_FieldCount, s_Fields(), 52) ' Early Start
			i_FieldPos_TaskEarlyFinishDate = f_FindField(i_FieldCount, s_Fields(), 53) ' Early Finish
			i_FieldPos_TaskLateStartDate = f_FindField(i_FieldCount, s_Fields(), 54) ' Late Start
			i_FieldPos_TaskLateFinishDate = f_FindField(i_FieldCount, s_Fields(), 55) ' Late Finish
		 	i_FieldPos_TaskDuration = f_FindField(i_FieldCount, s_Fields(), 40) ' Duration
		 	i_FieldPos_TaskActualDuration = f_FindField(i_FieldCount, s_Fields(), 42) ' Actual Duration
		 	i_FieldPos_TaskConstraintDate = f_FindField(i_FieldCount, s_Fields(), 68) ' Constraint Date
		 	i_FieldPos_TaskConstraintType = f_FindField(i_FieldCount, s_Fields(), 91) ' Constraint Type
		 	i_FieldPos_TaskPredecessors = f_FindField(i_FieldCount, s_Fields(), 70) ' Predecessors list
		 	i_FieldPos_TaskSuccessors = f_FindField(i_FieldCount, s_Fields(), 71) ' Successors list
		 	i_FieldPos_TaskMilestone = f_FindField(i_FieldCount, s_Fields(), 81) ' Milestone flag

		Case mpx_RN_Task ' (70) Task record
			' Create some temporary storage
			Dim i_taskSD As Date ' Start Date
			i_taskSD = 0 ' initialise to null date
			Dim i_taskFD As Date ' Finish date
			i_taskFD = 0
			Dim i_taskESD As Date ' early start date
			i_taskESD = 0
			Dim i_taskEFD As Date ' early finish date
			i_taskEFD = 0
			Dim i_taskLSD As Date ' late start date
			i_taskLSD = 0
			Dim i_taskLFD As Date ' Late Finish date
			i_taskLFD = 0
			Dim i_taskCD As Date ' constraint date
			i_taskCD = 0
			Dim s_taskCT As String ' constraint type
			s_taskCT = ""
			o_Project1.Tasks.Add(T_4) ' add an untitled Task
			With o_Project1.Tasks.Item(o_Project1.Tasks.Count)
				If (i_FieldPos_TaskID > 0) And (i_FieldCount >= i_FieldPos_TaskID) Then _
					.ID = CLng(Trim(s_Fields(i_FieldPos_TaskID)))
				If (i_FieldPos_TaskName > 0) And (i_FieldCount >= i_FieldPos_TaskName) Then _
					.Name = s_Fields(i_FieldPos_TaskName)
				If (i_FieldPos_TaskOutlineLevel > 0) And (i_FieldCount >= i_FieldPos_TaskOutlineLevel) Then _
					.OutlineLevel = CInt(Trim(s_Fields(i_FieldPos_TaskOutlineLevel)))
				If (i_FieldPos_TaskPriority > 0) And (i_FieldCount >= i_FieldPos_TaskPriority) Then _
					.Priority = f_MPXToPriority(s_Fields(i_FieldPos_TaskPriority))
				If (i_FieldPos_TaskPercentComplete > 0) And (i_FieldCount >= i_FieldPos_TaskPercentComplete) Then _
					.PercentComplete = f_MPXToPercent(s_Fields(i_FieldPos_TaskPercentComplete))
				If (i_FieldPos_TaskStartDate > 0) And (i_FieldCount >= i_FieldPos_TaskStartDate) Then _
					i_taskSD = f_MPXToDate(s_Fields(i_FieldPos_TaskStartDate))
				If (i_FieldPos_TaskFinishDate > 0) And (i_FieldCount >= i_FieldPos_TaskFinishDate) Then _
					i_taskFD = f_MPXToDate(s_Fields(i_FieldPos_TaskFinishDate))
				If (i_FieldPos_TaskEarlyStartDate > 0) And (i_FieldCount >= i_FieldPos_TaskEarlyStartDate) Then _
					i_taskESD = f_MPXToDate(s_Fields(i_FieldPos_TaskEarlyStartDate))
				If (i_FieldPos_TaskLateStartDate > 0) And (i_FieldCount >= i_FieldPos_TaskLateStartDate) Then _
					i_taskLSD = f_MPXToDate(s_Fields(i_FieldPos_TaskLateStartDate))
				If (i_FieldPos_TaskEarlyFinishDate > 0) And (i_FieldCount >= i_FieldPos_TaskEarlyFinishDate) Then _
					i_taskEFD = f_MPXToDate(s_Fields(i_FieldPos_TaskEarlyFinishDate))
				If (i_FieldPos_TaskLateFinishDate > 0) And (i_FieldCount >= i_FieldPos_TaskLateFinishDate) Then _
					i_taskLFD = f_MPXToDate(s_Fields(i_FieldPos_TaskLateFinishDate))
				If (i_FieldPos_TaskDuration > 0) And (i_FieldCount >= i_FieldPos_TaskDuration) Then _
					.Duration = f_MPXToDuration(s_Fields(i_FieldPos_TaskDuration))
				If (i_FieldPos_TaskConstraintDate > 0) And (i_FieldCount >= i_FieldPos_TaskConstraintDate) Then _
					i_taskCD = f_MPXToDate(s_Fields(i_FieldPos_TaskConstraintDate))
				If (i_FieldPos_TaskConstraintType > 0) And (i_FieldCount >= i_FieldPos_TaskConstraintType) Then _
					s_taskCT = UCase(Trim(s_Fields(i_FieldPos_TaskConstraintType)))
				If (i_FieldPos_TaskPredecessors > 0) And (i_FieldCount >= i_FieldPos_TaskPredecessors) Then _
					.Predecessors = UCase(Trim(s_Fields(i_FieldPos_TaskPredecessors)))
				If (i_FieldPos_TaskSuccessors > 0) And (i_FieldCount >= i_FieldPos_TaskSuccessors) Then _
					.Successors = UCase(Trim(s_Fields(i_FieldPos_TaskSuccessors)))
				If (i_FieldPos_TaskMilestone > 0) And (i_FieldCount >= i_FieldPos_TaskMilestone) Then
					.IsMilestone = f_MPXToFlag(s_Fields(i_FieldPos_TaskMilestone))
				Else ' no milestone received in the incoming record: infer it from the Duration
					.IsMilestone = (.Duration = 0) ' Assume a milestone if duration is zero
				End If
				' Set up start date
				.StartDate = 0 ' assume no start date
				If i_ImportSchedule = 0 Then ' importing constraint data only
					Select Case s_taskCT ' evaluate constraints
						Case UCase(T_357) ' Start No Earlier Than
							.StartDate = i_taskCD ' use Constraint Date if given
						Case UCase(T_356) ' Must Start On
							.StartDate = i_taskCD ' use Constraint Date if given
						Case UCase(T_358) ' Start No Later Than
							.StartDate = i_taskCD ' use Constraint Date if given
					End Select
				Else ' importing scheduled data
					If .StartDate = 0 Then .StartDate = i_taskSD ' else use scheduled Start Date
					If .StartDate = 0 Then .StartDate = i_taskESD ' else use Early Start Date
					If .StartDate = 0 Then .StartDate = i_taskLSD ' else use Late Start Date
				End If
				' Set up finish date
				.DeadlineDate = 0 ' assume no deadline date
				If i_ImportSchedule = 0 Then ' importing constraint data only
					Select Case s_taskCT ' evaluate constraints
						Case UCase(T_353) ' Finish No Earlier Than
							.DeadlineDate = i_taskCD ' use Constraint Date if given
						Case UCase(T_355) ' Must Finish On
							.DeadlineDate = i_taskCD ' use Constraint Date if given
						Case UCase(T_354) ' Finish No Later Than
							.DeadlineDate = i_taskCD ' use Constraint Date if given
					End Select
				Else ' importing task schedule data
					If .DeadlineDate = 0 Then .DeadlineDate = i_taskFD ' else use scheduled Finish Date
					If .DeadlineDate = 0 Then .DeadlineDate = i_taskLFD ' else use Late Finish Date
					If .DeadlineDate = 0 Then .DeadlineDate = i_taskEFD ' else use Early Finish date
				End If
			End With

		Case mpx_RN_TaskNotes ' (71) Task notes
			If i_FieldCount >= 2 Then
				If o_Project1.Tasks.Count > 0 Then ' append to newest task
					If i_FieldCount >= 2 Then _
						o_Project1.Tasks.Item(o_Project1.Tasks.Count).Notes = s_Fields(2) ' [2] Notes
				End If
			End If

		Case mpx_RN_RecurringTask ' (72) Recurring task information

		Case mpx_RN_ResourceAssignment ' (75) Resource assignment for current (latest) task
			If i_FieldCount >= 2 Then ' there is an ID present
				i_1 = CLng(Trim(s_Fields(2))) ' get [2] ID
				i_1 = o_Project1.Resources.FindbyID(i_1) ' look up its index
				If i_1 > 0 Then ' This Resource ID has been declared and we can assign it
					o_Project1.Assignments.Add(o_Project1.Tasks.Count, i_1) ' add new Assignment to current task
				End If
			End If

		Case mpx_RN_AssignmentWorkgroup ' (76) Assignment Workgroup fields

		Case mpx_RN_ProjectNames ' (80) Project names

		Case mpx_RN_DDEOLELinks ' (81) DDE and OLE links

		Case mpx_RN_Comment ' (0) Comment field

		Case Else ' Unrecognised MPX Record Number
			Err.Raise(1000,,Replace(Replace(T_27, "%1", s_1), "%2", s_Fields(0)),,)

	End Select

End Sub

Function f_FindField(ByVal i_FieldCount As Integer, ByRef s_Fields() As String, ByVal v_Find As Variant)
' Find a string or integer in the list of fields and return its index
' Return zero if not found, else return its position
Dim i_1 As Integer

	If Not (VarType(v_Find) = vbInteger) Then
		' not a numeric field: search for a string match
		v_Find = UCase(CStr(v_Find))
		For i_1 = 1 To i_FieldCount
			If v_Find = UCase(s_Fields(i_1)) Then
				f_FindField = i_1
				Exit Function
			End If
		Next i_1
	Else ' search for an integer field
		v_Find = CInt(v_Find)
		For i_1 = 1 To i_FieldCount
			If IsNumeric(Trim(s_Fields(i_1))) Then
				If v_Find = CInt(Trim(s_Fields(i_1))) Then
					f_FindField = i_1
					Exit Function
				End If
			End If
		Next i_1
	End If
	f_FindField = 0 ' not found

End Function

Function f_ExportMPX() As Boolean
' Export project object to a MPX file
' Returns True if successful, False else
Dim s_MPXfile As String ' MPX filename
Dim i_1 As Long
Dim i_2 As Long
Dim i_filter As Long
Dim i_taskSD As Date ' task start date
Dim i_taskCD As Date ' task constraint date
Dim s_taskCT As String ' task constraint type
Dim i_taskFD As Date ' task finish date

	f_ExportMPX = False ' assume cancellation or failure
	' Prompt user for filename to save
	s_MPXfile = f_CleanBranchName(m_wkgTopic, True) & "." & s_MPXExt ' default filename

	' Prepare file dialog
	Dim fd As FileDialog
	Set fd = FileDialog(mmFileDialogSaveAs)
	fd.Reset

	' Remove all existing filters
	Dim filterCount As Long
	filterCount = fd.Filters.Count
	For i_filter = 0 To filterCount - 1
		fd.Filters.Delete 1
	Next i_filter

	' Prepare file dialog options
	fd.AllowMultiSelect = False
	fd.PreviewPane = False
	fd.ButtonName = T_41
	fd.Filters.Add(T_60, s_MPXExtLong)
	fd.FilterIndex = fd.Filters.Count
	fd.InitialFileName = s_MPXfile
	fd.Title = T_40
	If fd.Show Then
	    s_MPXfile = fd.Item
	Else
	    s_MPXfile = ""
	End If

	's_MPXfile = GetFilePath(s_MPXfile, s_MPXExt, f_DefaultPath(), T_40, 3) ' "Select MPX file To export"
	If Len(s_MPXfile) = 0 Then Exit Function ' user cancels export
	SaveSetting(s_AppName, s_KeySection, s_KeyMRU, s_MPXfile) ' save this filename as MRU file

	' Prepare the file contents
	s_MPXRecords = "" ' flush records set
	i_MPXRecordCount = 0 ' reset counter

	' (MPX) File creation record
	Call p_StartRecord(1, mpx_Magic) ' No record number
	Call f_AddToRecord(2, Me.Name & " " & f_getMMVersion()) ' MindManager name & version
	Call f_AddToRecord(3, "4.0") ' MPX version 4.0
	Call p_FinishRecord(4) ' record finished

	' (0) Copyright statement
	Call p_StartRecord(1, mpx_RN_Comment)
	Call f_AddToRecord(2, Replace(T_15, "%1", s_AppName))
	Call p_FinishRecord(2)

	' (0) Declare Priority mapping scheme as a comment
	Call p_StartRecord(1, mpx_RN_Comment)
	Call f_AddToRecord(2, "Priority Mapping Scheme used")
	For i_1 = 0 To UBound(v_9PriorityNames)
		Call f_AddToRecord(3 + i_1, v_9PriorityNames(i_1))
	Next i_1
	Call p_FinishRecord(3 + UBound(v_9PriorityNames))

	' (0) Declare Duration Unit suffixes as a comment
	Call p_StartRecord(1, mpx_RN_Comment)
	Call f_AddToRecord(2, "Duration Unit Suffixes used")
	For i_1 = 0 To UBound(v_DurationNames)
		Call f_AddToRecord(3 + i_1, v_DurationNames(i_1))
	Next i_1
	Call p_FinishRecord(3 + UBound(v_DurationNames))

	' (0) Declare Yes and No flags as a comment
	Call p_StartRecord(1, mpx_RN_Comment)
	Call f_AddToRecord(2, "Yes and No flags used")
	Call f_AddToRecord(3, T_411) ' Yes
	Call f_AddToRecord(4, T_410) ' No
	Call p_FinishRecord(4)

	' (0) Declare Constraint Types as a comment
	Call p_StartRecord(1, mpx_RN_Comment)
	Call f_AddToRecord(2, "Constraint Types used")
	Call f_AddToRecord(3, T_351) ' list all constraint type texts used
	Call f_AddToRecord(4, T_352)
	Call f_AddToRecord(5, T_353)
	Call f_AddToRecord(6, T_354)
	Call f_AddToRecord(7, T_355)
	Call f_AddToRecord(8, T_356)
	Call f_AddToRecord(9, T_357)
	Call f_AddToRecord(10, T_358)
	Call p_FinishRecord(10)

	' (10) Currency settings record
	Call p_StartRecord(1, mpx_RN_Currency)
	Call GetLocaleInfo(0, LOCALE_SCURRENCY, s_WinBuf, Len(s_WinBuf)) ' read currency symbol
	Call f_AddToRecord(2, f_ReadWinBuf(s_WinBuf))
	Call GetLocaleInfo(0, LOCALE_SMONGROUPING, s_WinBuf, Len(s_WinBuf)) ' currency format
	Call f_AddToRecord(3, Left(f_ReadWinBuf(s_WinBuf), 1)) ' Window returns a strange format ("3;0")
	Call GetLocaleInfo(0, LOCALE_ICURRDIGITS, s_WinBuf, Len(s_WinBuf)) ' number of fractional digits
	Call f_AddToRecord(4, f_ReadWinBuf(s_WinBuf))
	Call GetLocaleInfo(0, LOCALE_SMONTHOUSANDSEP, s_WinBuf, Len(s_WinBuf)) ' read thousands separator
	Call f_AddToRecord(5, f_ReadWinBuf(s_WinBuf))
	Call f_AddToRecord(6, s_LocalDecimalSep) ' local decimal separator, taken from currency
	Call p_FinishRecord(6) ' record finished

	' (11) Default Settings record
	Call p_StartRecord(1, mpx_RN_Default)
	Call f_AddToRecord(2, o_Project1.TimeUnits) ' Duration units
	Call f_AddToRecord(3, 0) ' Durations not fixed
	Call f_AddToRecord(4, o_Project1.TimeUnits) ' Work units
	Call f_AddToRecord(5, Format(o_Project1.HoursPerDay, "0.0")) ' hours per day
	Call f_AddToRecord(6, Format(o_Project1.HoursPerDay * o_Project1.DaysPerWeek, "0.0")) '  hours per week
	Call f_AddToRecord(9, 1) '  Updating Task status updates Resource Status
	Call f_AddToRecord(10, 1) '  Split in-progress Tasks
	Call p_FinishRecord(10) ' record finished

	' (12) Date and Time Settings record
	Call p_StartRecord(1, mpx_RN_DateTime)
	Call f_AddToRecord(2, i_DateOrder) ' date field order
	Call GetLocaleInfo(0, LOCALE_ITIME, s_WinBuf, Len(s_WinBuf)) ' Time format specifier
	Call f_AddToRecord(3, f_ReadWinBuf(s_WinBuf))
	Call f_AddToRecord(4, 9 * 60) ' Default time (9am in minutes)
	Call f_AddToRecord(5, s_DateSep) ' Date separator
	Call GetLocaleInfo(0, LOCALE_STIME, s_WinBuf, Len(s_WinBuf)) ' Time separator
	Call f_AddToRecord(6, f_ReadWinBuf(s_WinBuf))
	Call GetLocaleInfo(0, LOCALE_S1159, s_WinBuf, Len(s_WinBuf)) ' AM designator
	Call f_AddToRecord(7, f_ReadWinBuf(s_WinBuf))
	Call GetLocaleInfo(0, LOCALE_S2359, s_WinBuf, Len(s_WinBuf)) ' PM desginator
	Call f_AddToRecord(8, f_ReadWinBuf(s_WinBuf))
	Call f_AddToRecord(9, i_DateFormat) ' date format
	Call p_FinishRecord(10) ' record finished

	' (30) Project Header record
	Call p_StartRecord(1, mpx_RN_ProjectHeader)
	Call f_AddToRecord(2, o_Project1.Name) ' Project Tab
	' Modification required for SureTrak: it will use today as the default start date if none specified, even if an end date is given
	'If o_Project1.ScheduleFromStart Then ' scheduled from project start
		Call f_AddToRecord(6, f_DateToMPX(o_Project1.StartDate)) ' Start Date (calculated of scheduled from end date)
	'Else
		Call f_AddToRecord(7, f_DateToMPX(o_Project1.FinishDate)) ' Finish Date (calculated if scheduled from start date)
	'End If
	Call f_AddToRecord(8, IIf(o_Project1.ScheduleFromStart, 0, 1)) ' Schedule from start (0) or end (1)
	Call f_AddToRecord(9, f_DateToMPX(Now())) ' Current date
	Call f_AddToRecord(10, Left(o_Project1.Notes, i_MaxNotesLen)) ' Comments: taken from the root branch TextNotes
	Call f_AddToRecord(28, m_wkgDoc.Name & " " & m_wkgDoc.Properties.Version) ' Subject: provide the source filename & revision
	Call f_AddToRecord(29, m_wkgDoc.Properties.AuthorName) ' Author: taken from document properties
	Call p_FinishRecord(30) ' record finished

	If o_Project1.Resources.Count > 0 Then
		' (40) Text Resource Table Definition Record
		Call p_StartRecord(1, mpx_RN_TextResources)
		Call f_AddToRecord(2, T_301) ' ID
		Call f_AddToRecord(3, T_300) ' Name
		Call f_AddToRecord(4, T_302) ' Initials
		Call p_FinishRecord(4)
		' (41) Numeric Resource Table Definition Record
		Call p_StartRecord(1, mpx_RN_NumericResources)
		Call f_AddToRecord(2, 40) ' ID
		Call f_AddToRecord(3, 1) ' Name
		Call f_AddToRecord(4, 2) ' Initials
		Call p_FinishRecord(4)
		' (50) Resource Records
		For i_1 = 1 To o_Project1.Resources.Count
			Call p_StartRecord(1, mpx_RN_Resource) ' Resource record
			With o_Project1.Resources.Item(i_1)
				Call f_AddToRecord(2, .ID) ' Resource ID
				Call f_AddToRecord(3, .Name) ' Resource Name
				Call f_AddToRecord(4, .Initials) ' Resource Initials
			End With
			Call p_FinishRecord(4)
		Next i_1
	End If

	' Task Table Definition records
	If o_Project1.Tasks.Count > 0 Then
		' (60) Text Task Table Definition record
		Call p_StartRecord(1, mpx_RN_TextTaskTableDef)
		Call f_AddToRecord(2, T_311) ' ID
		Call f_AddToRecord(3, T_312) ' Outline level
		Call f_AddToRecord(4, T_310) ' Name
		Call f_AddToRecord(5, T_313) ' Priority
		Call f_AddToRecord(6, T_314) ' % Complete
		Call f_AddToRecord(7, T_315) ' % Work Complete
		Call f_AddToRecord(8, T_316a) ' Start
		Call f_AddToRecord(9, T_316b) ' Finish
		Call f_AddToRecord(10, T_316g) ' Baseline Start
		Call f_AddToRecord(11, T_316h) ' Baseline Finish
		Call f_AddToRecord(12, T_317) ' Duration
		Call f_AddToRecord(13, T_318) ' Actual Duration
		Call f_AddToRecord(14, T_319) ' Constraint Date
		Call f_AddToRecord(15, T_320) ' Constraint type
		Call f_AddToRecord(16, T_321) ' Summary
		Call f_AddToRecord(17, T_322) ' Rollup
		Call f_AddToRecord(18, T_323) ' WBS
		Call f_AddToRecord(19, T_324) ' Predecessors (always mentioned even if not used)
		Call f_AddToRecord(20, T_325) ' Successors (always mentioned even if not used)
		Call f_AddToRecord(21, T_326) ' Milestone
		Call p_FinishRecord(21)
		' (61) Numeric Task Table Definition Record
		Call p_StartRecord(1, mpx_RN_NumericTaskTableDef)
		Call f_AddToRecord(2, 90) ' ID
		Call f_AddToRecord(3, 3) ' Outline Level
		Call f_AddToRecord(4, 1) ' Name
		Call f_AddToRecord(5, 95) ' Priority
		Call f_AddToRecord(6, 44) ' % Complete
		Call f_AddToRecord(7, 25) ' % Work Complete
		Call f_AddToRecord(8, 50) ' Start Date
		Call f_AddToRecord(9, 51) ' Finish date
		Call f_AddToRecord(10, 56) ' Baseline Start
		Call f_AddToRecord(11, 57) ' Baseline Finish
		Call f_AddToRecord(12, 40) ' Duration
		Call f_AddToRecord(13, 42) ' Actual Duration
		Call f_AddToRecord(14, 68) ' Constraint Date
		Call f_AddToRecord(15, 91) ' Constraint Type
		Call f_AddToRecord(16, 120) ' Summary
		Call f_AddToRecord(17, 84) ' Rollup
		Call f_AddToRecord(18, 2) ' WBS
		Call f_AddToRecord(19, 70) ' Predecessors list (always mentioned even if not used)
		Call f_AddToRecord(20, 71) ' Successors list (always mentioned even if not used)
		Call f_AddToRecord(21, 81) ' Milestone
		Call p_FinishRecord(21)
	End If

	' (70) Task Records
	For i_1 = 1 To o_Project1.Tasks.Count
		Call p_StartRecord(1, mpx_RN_Task)
		With o_Project1.Tasks.Item(i_1)
			Call f_AddToRecord(2, .ID) ' task ID
			Call f_AddToRecord(3, .OutlineLevel) ' task Outline Level
			Call f_AddToRecord(4, .Name) ' Task Name
			Call f_AddToRecord(5, f_PriorityToMPX(.Priority)) ' Task Priority
			If Not .IsSummary Then
				Call f_AddToRecord(6, f_PercentToMPX(.PercentComplete)) ' % Complete
				Call f_AddToRecord(7, f_PercentToMPX(.PercentComplete)) ' % Work Complete
			End If
			' Set up task start date, deadline and constraint type
			i_taskSD = .StartDate ' set defaults
			i_taskFD = .DeadlineDate
			If .DeadlineDate <> 0 Then ' deadline date is constraint
				i_taskCD = .DeadlineDate ' constraint date
				s_taskCT = IIf(o_Project1.ScheduleFromStart, T_353, T_354) ' FNET if scheduled from Start, FNLT else
			ElseIf .StartDate <> 0 Then ' no Due date specified but Start date specified
				i_taskCD = .StartDate ' start date is the constraint
				s_taskCT = IIf(o_Project1.ScheduleFromStart, T_357, T_358) ' SNET if scheduled from Start, SNLT else
			Else ' no explicit constraint has been given
				i_taskCD = 0 ' no constraint date
				s_taskCT = IIf(o_Project1.ScheduleFromStart, T_352, T_351) ' ASAP if scheduled from start, ALAP else
			End If
			' Estimate any incomplete date info
			Call p_Elapsed(.Duration, o_Project1.StartDate, i_taskSD, i_taskFD)
			Call f_AddToRecord(8, f_DateToMPX(i_taskSD)) ' Start Date
			Call f_AddToRecord(9, f_DateToMPX(i_taskFD)) ' Finish Date
			Call f_AddToRecord(10, f_DateToMPX(i_taskSD)) ' Baseline Start date = Scheduled start date
			Call f_AddToRecord(11, f_DateToMPX(i_taskFD)) ' Baseline Finish date = Scheduled finish date
			If Not .IsSummary Then ' only write durations for tasks
				Call f_AddToRecord(12, f_DurationToMPX(.Duration)) ' Duration
				Call f_AddToRecord(13, f_DurationToMPX(.Duration * (.PercentComplete / 100))) ' Actual Duration
			End If
			Call f_AddToRecord(14, f_DateToMPX(i_taskCD)) ' Constraint Date
			Call f_AddToRecord(15, s_taskCT) ' Constraint Type
			Call f_AddToRecord(16, f_FlagToMPX(.IsSummary)) ' Summary flag
			Call f_AddToRecord(17, f_FlagToMPX(.Rollup)) ' Rollup flag
			Call f_AddToRecord(18, .WBS) ' WBS info
			If (i_DoRelationships = 1) Then
				Call f_AddToRecord(19, .Predecessors) ' Show Predecessor list
			ElseIf (i_DoRelationships = 2) Then
				Call f_AddToRecord(20, .Successors) ' Show Successor list
			End If
			If b_UseMilestoneCode Then Call f_AddToRecord(21, f_FlagToMPX(.IsMilestone)) ' write Milestone flag
		End With
		Call p_FinishRecord(21)

		' (71) Task Notes record
		If Len(o_Project1.Tasks.Item(i_1).Notes) > 0 Then
			Call p_StartRecord(1, mpx_RN_TaskNotes)
			Call f_AddToRecord(2, Left(o_Project1.Tasks.Item(i_1).Notes, i_MaxNotesLen)) ' task notes
			Call p_FinishRecord(2)
		End If
		If o_Project1.Resources.Count > 0 Then
			' (75) Resource Assignment Record for this task
			If o_Project1.Tasks.Item(i_1).HasResources Then
				For i_2 = 1 To o_Project1.Assignments.Count
					If o_Project1.Assignments.Item(i_2).TaskID = o_Project1.Tasks.Item(i_1).ID Then ' is this task
						Call p_StartRecord(1, mpx_RN_ResourceAssignment) ' Resource Assignment record
						Call f_AddToRecord(2, o_Project1.Assignments.Item(i_2).ResourceID) ' Resource ID
						Call f_AddToRecord(3, o_Project1.TimeUnits) ' Work units
						Call f_AddToRecord(4, f_DurationToMPX(o_Project1.Tasks.Item(i_1).Duration)) ' Work
						Call f_AddToRecord(6, f_DurationToMPX(o_Project1.Tasks.Item(i_1).Duration * _
								(o_Project1.Tasks.Item(i_1).PercentComplete / 100))) ' Actual Work
						Call p_FinishRecord(14)
					End If
				Next i_2
			End If
		End If
	Next i_1

	' Ready: write file
	Call p_writeFile(s_MPXfile, s_MPXRecords)
	If b_Debug Then Call p_Dump(s_MPXfile) ' dump MPX file
	f_ExportMPX = True ' finished OK

End Function

Sub p_StartRecord(ByVal i_FieldPos As Integer, ByVal s_RecordNumber As Variant)
' Start a new MPX file record

	s_MPXRecord = CStr(s_RecordNumber) ' Write first field
	i_MPXField = i_FieldPos + 1 ' start adding at next field
	i_MPXRecordCount = i_MPXRecordCount + 1 ' count this record

End Sub

Function f_AddToRecord(ByVal i_FieldPos As Integer, ByVal v_Value As Variant) As Boolean
' Add another value to the current record
' Make substitutions and escape reserved characters
' This routine cannot be used to start a record, as it adds the list separator
' Returns True if something written, False else
Dim b_quote As Boolean

	' Align to required field position
	If i_FieldPos <> i_MPXField Then ' not in right position to write next field
		If i_FieldPos < i_MPXField Then Err.Raise(1000,,T_28,,) ' internal sync error
		While i_FieldPos > i_MPXField
			s_MPXRecord = s_MPXRecord & s_MPXsep ' output separator to move forward
			i_MPXField = i_MPXField + 1
		Wend
	End If
	If Not VarType(v_Value) = vbString Then v_Value = CStr(v_Value) ' convert to string
	' Double up any speech marks existing in original text
	If InStr(v_Value, mpx_Cquote) > 0 Then v_Value = Replace(v_Value, mpx_Cquote, mpx_Cquote & mpx_Cquote)
	b_quote = False ' assume it does not need to be quoted
	If InStr(v_Value, vbCrLf) > 0 Then v_Value = Replace(v_Value, vbCrLf, mpx_Cnewline) ' substitute CRLFs
	If Trim(v_Value) <> v_Value Then b_quote = True ' has leading or trailing spaces, needs quoting
	If InStr(v_Value, s_MPXsep) > 0 Then b_quote = True ' needs quoting
	f_AddToRecord = Len(v_Value) > 0 ' return True if something written
	If b_quote Then v_Value = mpx_Cquote & v_Value & mpx_Cquote ' wrap it in quotes
	s_MPXRecord = s_MPXRecord & s_MPXsep & v_Value ' add separator and value to record
	i_MPXField = i_MPXField + 1 ' prepare for next field

End Function

Function f_DateToMPX(ByVal i_Date As Date) As String
' Convert MM Date to MPX Date

	If i_Date = 0 Then ' no date defined, do not publish one
		f_DateToMPX = "" ' no output
	Else ' date is valid: publish it in default format
		Select Case i_DateOrder ' we do not rely on system short date format
			Case 0 ' Month/Day/Year
				f_DateToMPX = Format(i_Date, "m/d/yy")
			Case 1 ' Day/Month/Year
				f_DateToMPX = Format(i_Date, "d/m/yy")
			Case Else ' Year/Month/Day
				f_DateToMPX = Format(i_Date, "yy/m/d")
		End Select
	End If

End Function

Function f_MPXToDate(ByVal s_Date As String) As Date
' Convert MPX date string to MM date value
' Returns 0 if no date information
' Requires that the date format is already set
Dim s_1 As String
Dim b_ready As Boolean
Dim i_format As Integer
Dim i_1 As Integer
Dim i_2 As Integer
Dim s_dateparts(1 To 3) As String ' date parts
Dim v_datePtrs As Variant ' array of date part pointers

	s_1 = Trim(s_Date)
	s_1 = Replace(s_1, Chr(39), "") ' drop any apostrophes
	s_1 = Replace(s_1, ",", "") ' drop any commas
	If Len(s_1) = 0 Then
		f_MPXToDate = 0 ' return nothing
		Exit Function
	End If
	' Set up the mask for the incoming date string
	b_ready = False
	i_format = i_DateFormat
	While Not b_ready
		Select Case i_format
			Case 0 ' 31.1.97 12:33
				s_1 = Replace(Trim(s_1), " ", s_DateSep)
				b_ready = True ' ready to parse
			Case 1 ' 31.1.97
				s_1 = s_1 & s_DateSep ' append closing separator
				i_format = 0 ' treat as type 0
			Case 20 ' 31.1.1997
				i_format = 1 ' treat as type 1
			Case 2 ' 31 January 1997 12:33
				i_format = 0 ' treat as type 0
			Case 3 ' 31 January 1997
				i_format = 1 ' treat as type 1
			Case 5 ' 31 Jan '97
				i_format = 1
			Case 8 ' Fri 31.1.97 12:33
				i_1 = InStr(s_1, " ") ' look for first space
				If i_1 > 0 Then s_1 = Right(s_1, Len(s_1) - i_1)
				i_format = 0 ' treat as type 0
			Case 9 ' Fri 31.1.97
				s_1 = s_1 & s_DateSep ' append separator
				i_format = 8 ' treat as type 8
			Case 10 ' Fri 31 Jan '97
				i_format = 9 ' treat as type 9
			Case Else ' insufficient or unsupported format
				Err.Raise(1000,,Replace(T_23, "%1", s_Date),,)
				b_ready = True
		End Select
	Wend
	' Now extract date info
	For i_1 = 1 To 3 ' 3 parts
		i_2 = InStr(s_1, s_DateSep) ' look for next separator
		If i_2 > 0 Then
			s_dateparts(i_1) = UCase(Trim(Left(s_1, i_2 - 1)))
			s_1 = Right(s_1, Len(s_1) - i_2)
		Else
			Exit For ' not found
		End If
	Next i_1
	' Set up the correct order of elements
	Select Case i_DateOrder
		Case 0 ' Month/Day/Year
			v_datePtrs = Array(0, 2, 1, 3) ' Pointers to day/month/year
		Case 1 ' Day/Month/Year
			v_datePtrs = Array(0, 1, 2, 3) ' Pointers to day/month/year
		Case Else ' Year/Month/Day
			v_datePtrs = Array(0, 3, 2, 1) ' Pointers to day/month/year
	End Select
	' Decode Month if not numeric; this may fail on text months in other languages
	If Not IsNumeric(s_dateparts(v_datePtrs(2))) Then
		For i_1 = 1 To 12
			If (s_dateparts(v_datePtrs(2)) = s_LongMonths(i_1)) Or (s_dateparts(v_datePtrs(2)) = s_ShortMonths(i_1)) Then
				s_dateparts(v_datePtrs(2)) = CStr(i_1)
				Exit For
			End If
		Next i_1
	End If
	' Decode date only if month has been decoded successfully
	f_MPXToDate = 0 ' assume no date
	If IsNumeric(s_dateparts(v_datePtrs(2))) Then
		On Error Resume Next
		f_MPXToDate = DateSerial(CInt(s_dateparts(v_datePtrs(3))), CInt(s_dateparts(v_datePtrs(2))), CInt(s_dateparts(v_datePtrs(1))))
		If Err.Number <> 0 Then f_MPXToDate = 0 ' no date was decoded
		Err.Clear
		On Error GoTo 0 ' restore error handling
	End If

End Function

Function f_DurationToMPX(ByVal i_Duration As Long) As String
' Export a duration to MPX format
' Exported durations are always Longs and never fractional

	f_DurationToMPX = CStr(i_Duration) & v_DurationNames(o_Project1.TimeUnits) ' assume it is in default units

End Function

Function f_MPXToDuration(ByVal s_Duration As String) As Long
' Import a MPX duration value
Dim i_1 As Integer
Dim i_Units As Integer

	s_Duration = UCase(Trim(s_Duration))
	If Len(s_Duration) = 0 Then ' not defined
		f_MPXToDuration = 0
		Exit Function
	End If

	' replace remote with local decimal separator
	If s_DecimalSep <> s_LocalDecimalSep Then _
		s_Duration = Replace(s_Duration, s_DecimalSep, s_LocalDecimalSep)

	' Establish the duration units
	i_Units = -1 ' assume not found
	For i_1 = 0 To UBound(v_DurationNames) ' Check all types of duration unit supported
		If Len(s_Duration) >= Len(v_DurationNames(i_1)) Then
			If Right(s_Duration, Len(v_DurationNames(i_1))) = UCase(v_DurationNames(i_1)) Then
				i_Units = i_1 ' matched duration units
				Exit For ' leave loop
			End If
		End If
	Next i_1
	If i_Units < 0 Then i_Units = o_Project1.TimeUnits ' default to project time units if not matched

	' Convert duration to local units
	Select Case i_Units
		Case 0 ' minutes
			f_MPXToDuration = CLng(Round(Val(s_Duration) / 60)) ' convert to hours and round
		Case 2 ' days
			f_MPXToDuration = CLng(Val(s_Duration) * o_Project1.HoursPerDay) ' convert to hours
		Case 3 ' weeks
			f_MPXToDuration = CLng(Val(s_Duration) * o_Project1.DaysPerWeek * o_Project1.HoursPerDay) ' convert to hours
		Case Else ' hours
			f_MPXToDuration = CLng(Val(s_Duration)) ' already in hours
	End Select

End Function

Function f_PercentToMPX(ByVal i_Percent As Integer) As String
' Export a percentage value to MPX

	f_PercentToMPX = CStr(i_Percent) & T_230 ' append percent character

End Function

Function f_MPXToPercent(ByVal s_Percent As String) As Integer
' Import a percentage value from MPX
' MS Project may append a "%" character

	s_Percent = Trim(s_Percent)
	If Len(s_Percent) = 0 Then ' no data
		f_MPXToPercent = 0
	Else
		f_MPXToPercent = CInt(Round(Val(s_Percent))) ' round to integer, ignore any trailing characters
	End If

End Function

Function f_PriorityToMPX(ByVal i_Priority As Integer) As String
' Convert MM Priority to MPX Priority
' There is no equivalent to the "no priority" setting in MS Project 98
Dim v_names As Variant ' Priority names

	f_PriorityToMPX = "" ' assume none to report
	If i_Priority <= 0 Then Exit Function
	If i_Priority > (UBound(v_9PriorityNames) + 1) Then i_Priority = (UBound(v_9PriorityNames) + 1) ' apply limits
	f_PriorityToMPX = v_9PriorityNames(i_Priority - 1) ' convert

End Function

Function f_MPXToPriority(ByVal s_Priority As String) As Integer
' Convert MPX Priority to MM Priority
Dim i_1 As Integer
Dim i_2 As Long

	f_MPXToPriority = i_DefaultPriority ' assume none
	s_Priority = UCase(Trim(s_Priority))
	If Len(s_Priority) = 0 Then Exit Function ' no priority
	' Try mapping priority names
	For i_1 = 0 To UBound(v_9PriorityNames)
		If UCase(v_9PriorityNames(i_1)) = s_Priority Then
			If i_1 > 8 Then i_1 = 8 ' off end of list
			f_MPXToPriority = i_1 + 1
			Exit Function
		End If
	Next i_1

End Function

Function f_FlagToMPX(ByVal b_Flag As Boolean) As String
' Convert a boolean Flag to MPX

	f_FlagToMPX = IIf(b_Flag, T_411, T_410) ' Convert to language-dependent text

End Function

Function f_MPXToFlag(ByVal s_Flag As String) As Boolean
' Convert a Yes/No String to a boolean flag

	f_MPXToFlag = (UCase(Trim(s_Flag)) = UCase(Trim(T_411))) ' True if it matches the Yes string

End Function

Sub p_FinishRecord(ByVal i_FieldPos As Integer)
' Finish the current record and add it to the records set
' If a FieldPos value is given, then pad out the record to this number of fields

	If i_FieldPos <> i_MPXField Then ' not in right position to finish record
		If (i_FieldPos + 1) < i_MPXField Then Err.Raise(1000,,T_28,,) ' internal field sync error
		While i_FieldPos > i_MPXField
			s_MPXRecord = s_MPXRecord & s_MPXsep ' output separator to move forward
			i_MPXField = i_MPXField + 1
		Wend
	End If
	s_MPXRecords = s_MPXRecords & s_MPXRecord & vbCrLf

End Sub

Sub p_Elapsed(ByVal i_Duration As Integer, ByVal i_Default As Date, ByRef i_Start As Date, ByRef i_Finish As Date)
' Estimate elapsed duration for a task, adjust for no. of days per week and weekends
' Duration is assumed to be in hours
Dim i_1 As Integer
Dim i_dpw As Integer ' days per week
Dim i_dow As Integer ' day of week
Dim i_Estimated As Date ' estimated date
Dim b_AdjustFinish As Boolean

	i_dpw = o_Project1.HoursPerDay ' fetch hours per day
	If i_dpw <= 0 Then i_dpw = 8 ' default to 8 hours per day if an invalid number
	i_Duration = Int(i_Duration / i_dpw) ' convert hours to days
	i_dpw = o_Project1.DaysPerWeek ' fetch days per week
	If i_dpw <= 0 Then i_dpw = 5 ' default to 5 if an invalid number
	i_1 = Int(i_Duration/i_dpw) ' number of whole weeks
	i_Duration = (i_1 * 7) + (i_Duration - (i_1 * i_dpw)) ' convert to duration
	i_Estimated = 0 ' assume no estimation taken place
	b_AdjustFinish = True ' assume we adjust finish date
	' Check to see if an estimate is needed
	If i_Start = 0 Then ' start date is missing
		If i_Finish = 0 Then ' finish date is also missing
			i_Start = i_Default ' use default start date
			If i_Start = 0 Then i_Start = Date() ' use today as the final default start date
			i_Finish = DateAdd("d", i_Duration, i_Start) ' estimate finish date
			i_Estimated = i_Finish
		Else ' estimate start date
			i_Start = DateAdd("d", 0 - i_Duration, i_Finish)
			i_Estimated = i_Start
			b_AdjustFinish = False
		End If
	ElseIf i_Finish = 0 Then ' finish date is missing
		i_Finish = DateAdd("d", i_Duration, i_Start) ' estimate finish date from start
		i_Estimated = i_Finish ' estimated finish date
	End If
	' Adjust final finishing day of week
	If i_Estimated <> 0 Then
		i_dow = Weekday(i_Estimated) ' fetch weekday of estimated date
		i_dow = i_dow - 1 ' adjust so that Monday is day 1
		If i_dow < 1 Then i_dow = i_dow + 7 ' wrap around
		If b_AdjustFinish Then ' adjust finish date
			For i_1 = (i_dpw + 1) To 7
				If i_dow = i_1 Then
					i_Finish = DateAdd("d", 1, i_Finish)
					i_dow = i_dow + 1 ' move day forwards
				End If
			Next i_1
		Else ' adjust start date
			For i_1 = 7 To (i_dpw + 1) Step -1
				If i_dow = i_1 Then ' starts at weekend
					i_Start = DateAdd("d", -1, i_Start) ' move a day earlier
					i_dow = i_dow - 1 ' move weekday
				End If
			Next i_1
		End If
	End If

End Sub

 ' Sub p_ImportBranch(ByVal m_b1 As Branch, ByVal i_level As Integer, ByVal s_WBS As String)
Sub p_ImportBranch(ByVal m_b1 As Topic, ByVal i_level As Integer, ByVal s_WBS As String) ' [1-003372] "As Branch" : Branch becomes Topic
' Read task branches from the map
' Adds task for all children of the selected branch
' Level 0 is the project root level
Dim i_1 As Long
Dim i_TaskPtr As Integer ' current task pointer
Dim i_ResourcePtr As Integer ' assigned resource pointer
Dim s_1 As String
Dim s_2 As String
Dim s_parts As Variant ' resources list array
Dim r_1 As Relationship
Dim v_here As MmConnectionShape ' arrowhead at this end of relationship
Dim v_there As MmConnectionShape ' arrowhead at remote end of relationship
Dim m_b2 As Topic ' [1-003372] "As Branch" : Branch becomes Topic

	If i_level = 0 Then ' this is the project root branch
		With o_Project1
			.Name = f_CleanBranchName(m_b1, False) ' Project name
			.Notes = m_b1.Notes.Text ' ASCII textnotes
			If f_HasStartDate(m_b1) Then
				.StartDate = m_b1.Task.StartDate
			Else
				.StartDate  = 0 ' no project start date
			End If
			If f_HasDeadlineDate(m_b1) Then
				.FinishDate = m_b1.Task.DueDate ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"
			Else
				.FinishDate = 0 ' no project finish date
			End If
		End With
	End If

	If (i_level > 0) Or b_IncludeProjectRollup Then ' Create a task for this branch
		i_TaskPtr = o_Project1.Tasks.Add(f_CleanBranchName(m_b1, False)) ' Create new task
		' Populate task info
		With o_Project1.Tasks.Item(i_TaskPtr) ' newly added task
			.BranchID = f_HashGuid(m_b1.Guid) ' save branch ID for this task
			.BranchGuid = m_b1.Guid ' save original Guid
			.Notes = m_b1.Notes.Text ' ASCII textnotes
			' Outline level starts at 1, with the project root if including a project-level rollup
			.OutlineLevel = IIf(b_IncludeProjectRollup, i_level + 1, i_level) ' outline level
			.IsSummary = (m_b1.SubTopics(False).Count > 0) ' Is a summary if it has children
			.Rollup = .IsSummary And (i_level > 0) ' Rollup summaries unless at project root
			.WBS = IIf((i_level = 0), "0", s_WBS) ' Output WBS "0" at project root level
			' Convert priority level to 9-level on export
			If m_b1.Task.Priority > 0 Then ' [1-000980] ".Priority" : See "How to find all branch properties"
				.Priority = m_b1.Task.Priority ' no conversion
			Else ' no priority stated, use default
				.Priority = i_DefaultPriority
			End If
			' If we are exporting a valid project only, then some fields can only
			' be specified if there are no child tasks
			If m_b1.SubTopics(False).Count = 0 Then ' this is a Task
				i_1 = m_b1.Task.DurationInHours ' read branch duration
				If b_UseMilestoneCode And m_b1.Icons(True).HasStockIcon(mmStockIconClock) Then ' this is a milestone
					.IsMilestone = True
					i_1 = 0 ' force zero duration if milestone code present
				Else ' duration should not be zero
					If i_1 = 0 Then i_1 = o_Project1.HoursPerDay ' set default task time of 1 day
				End If
				.Duration = i_1 ' set task duration
				.PercentComplete = m_b1.Task.Complete ' [1-001058] ".PercentageDone" : See "How to find all branch properties"
				If .PercentComplete < 0 Then .PercentComplete = 0 ' no value = 0
			End If
			' Add dates if they are set
			If f_HasStartDate(m_b1) Then .StartDate = m_b1.Task.StartDate ' [1-001002] ".StartDate" : See "How to find all branch properties"
			If f_HasDeadlineDate(m_b1) Then .DeadlineDate = m_b1.Task.DueDate ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"

			' Add relationships to predecessor and successor tasks
			' These are initially added using the remote branch IDs
			' They are converted to task IDs later, which also filters out any links not in the project tree
			For i_1 = 1 To m_b1.Relationships(True).Count
				Set r_1 = m_b1.Relationships(True).Item(i_1) ' fetch relationships
				v_here = mmConnectionShapeNoArrow
				v_there = mmConnectionShapeNoArrow
				Set m_b2 = Nothing
				' Work out which end is which
				If r_1.ConnectedObject1 Is m_b1 Then ' end 1 is us
					If r_1.ConnectedObject2.Type = mmDocumentObjectTypeTopic Then
						v_here = r_1.Shape1 ' local end
						v_there = r_1.Shape2 ' remote end
						Set m_b2 = r_1.ConnectedObject2 ' remote end
					End If
				Else ' end 2 is us
					If r_1.ConnectedObject1.Type = mmDocumentObjectTypeTopic Then
						v_here = r_1.Shape2 ' local end
						v_there = r_1.Shape1 ' remote end
						Set m_b2 = r_1.ConnectedObject1 ' remote end
					End If
				End If
				s_2 = "" ' assume no relationship callout
				If r_1.CalloutTopics(True).Count > 0 Then s_2 = r_1.CalloutTopics(True).Item(1).Text ' get text
				s_1 = f_ValidateTRD(Replace(s_2, s_MPXsep, "")) ' strip separator from text and validate it
				' Note that two-way relationship are only half processed
				If (v_here <> mmConnectionShapeNoArrow)  And (v_there = mmConnectionShapeNoArrow) Then ' arrow points to us, predecessor task
					If Len(.Predecessors) > 0 Then .Predecessors = .Predecessors & s_MPXsep ' append separator
					.Predecessors = .Predecessors & CStr(f_HashGuid(m_b2.Guid)) & s_1
				ElseIf (v_here = mmConnectionShapeNoArrow) And (v_there <> mmConnectionShapeNoArrow) Then ' arrow points away, successor task
					If Len(.Successors) > 0 Then .Successors = .Successors & s_MPXsep ' append separator
					.Successors = .Successors & CStr(f_HashGuid(m_b2.Guid)) & s_1 '
				Else ' this is a two-way relationship; only export it once, end 1 -> end 2, as drawn
					If r_1.ConnectedObject2 Is m_b1 Then ' end 2 is us, [1-001648] ".Branch2" : A Relationship can now be connected to a Topic or a Boundary.; See "How to get an object's type"
						If Len(.Predecessors) > 0 Then .Predecessors = .Predecessors & s_MPXsep ' append separator
						.Predecessors = .Predecessors & CStr(f_HashGuid(m_b2.Guid)) & s_1 ' [3-001022] ".ObjectID" : See "How to deal with the Object ID"; See "How to deal with the Object ID"; See "How to deal with the Object ID"
					End If
				End If
			Next i_1
			Set m_b2 = Nothing ' clean up
			Set r_1= Nothing
		End With

		' Add resources
		If m_b1.SubTopics(False).Count = 0 Then ' resources only apply to tasks, not task containers, [6-000960] ".Count" : Ambiguous property. Check object type.
			s_1 = "" ' assume no resources
			s_1 = Trim(m_b1.Task.Resources) ' read resources from branch, [1-001110] ".Resources" : See "How to find all branch properties"
			If Len(s_1) = 0 Then s_1 = s_DefaultResource ' default resource allocation, if any
			If Len(s_1) > 0 Then ' resources are to be allocated
				's_parts = Split(s_1, s_ResourceSep) ' split up resource string
				s_parts = f_Split(s_1, s_ResourceSep) ' split up resource string
				For i_1 = 0 To UBound(s_parts)
					s_1 = Trim(s_parts(i_1)) ' read resource
					If Len(s_1) > 0 Then
						i_ResourcePtr = o_Project1.Resources.Add(s_1) ' record this resource
						Call o_Project1.Assignments.Add(i_TaskPtr, i_ResourcePtr) ' add an assignment here, ignore result
					End If
				Next i_1
			End If
		End If

		' Append WBS separator character
		If Len(s_WBS) > 0 Then s_WBS = s_WBS & T_231

	End If

	' Now import task data for the children of this branch
	For i_1 = 1 To m_b1.SubTopics(False).Count
		Call p_ImportBranch(m_b1.SubTopics(False).Item(i_1), i_level + 1, s_WBS & CStr(i_1)) ' [6-000922] ".Item" : Ambiguous property. Check object type.
	Next i_1

End Sub

 ' Function f_ValidateBranch(ByVal m_b1 As Branch, ByVal i_level As Integer, ByVal i_Vstart As Date, ByVal i_Vfinish As Date) As Boolean
Function f_ValidateBranch(ByVal m_b1 As Topic, ByVal i_level As Integer, ByVal i_Vstart As Date, ByVal i_Vfinish As Date) As Boolean ' [1-003372] "As Branch" : Branch becomes Topic
' Validate project branches and display error messages
' Returns True if user continues with export, False else
Dim i_1 As Integer
Dim s_Warn As String ' Warning message
 ' Dim m_b2 As Branch
Dim m_b2 As Topic ' [1-003372] "As Branch" : Branch becomes Topic
Dim r_1 As Relationship
Dim b_Continue As Boolean
Dim s_1 As String
Dim s_2 As String
Dim s_3 As String

	s_Warn = "" ' assume no error

	If i_level = 0 Then ' this is the project root branch
		If b_ValidateProject Then
			If m_b1.SubTopics(False).Count = 0 Then s_Warn = s_Warn & T_71 ' no tasks in project, [6-000960] ".Count" : Ambiguous property. Check object type.
			' Save the project start and end date if available
			If f_HasStartDate(m_b1) Then i_Vstart = m_b1.Task.StartDate ' [1-001002] ".StartDate" : See "How to find all branch properties"
			If f_HasDeadlineDate(m_b1) Then i_Vfinish = m_b1.Task.DueDate ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"
			If o_Project1.ScheduleFromStart Then ' there must be a start date here
				If (Not f_HasStartDate(m_b1)) Then
					If f_HasDeadlineDate(m_b1) Then ' but there is a finish date
						s_Warn = s_Warn & Replace(T_94, "%1", Format(m_b1.Task.DueDate, "ddddd")) ' switch over to schedule from Finish, [1-001006] ".DeadlineDate" : See "How to find all branch properties"
						o_Project1.ScheduleFromStart = False
					Else
						s_Warn = s_Warn & T_91 ' no start date found
					End If
				End If
			Else
				If (Not f_HasDeadlineDate(m_b1)) Then
					If f_HasStartDate(m_b1) Then ' switch over to scheduling from start
						s_Warn = s_Warn & Replace(T_95, "%1", Format(m_b1.Task.StartDate, "ddddd")) ' warn user that we are switching to schedule from Start, [1-001002] ".StartDate" : See "How to find all branch properties"
						o_Project1.ScheduleFromStart = True
					Else
						s_Warn = s_Warn & T_93 ' no finish date found
					End If
				End If
			End If
		End If
	Else ' not the project root branch
		' Validate non-project data
		If b_CheckExportable Then
			If Len(m_b1.Notes.Text) > i_MaxNotesLen Then ' oversize textnotes; CR 10527
				s_Warn = s_Warn & Replace(T_72, "%1", CStr(i_MaxNotesLen)) ' CR 10527
			End If
			' Note this branch cannot be the project root branch if we are here,
			' so hyperlink is valid under all conditions
			If m_b1.HasHyperlink Then ' there is a hyperlink ; CR 10522
				If Len(m_b1.Hyperlink.Address) > 0 Then s_Warn = s_Warn & T_80 ' Hyperlink wil be lost
			End If
			If Len(m_b1.Notes.Text) > 0 Then
				' Check for formatting in the textnotes
				If Not m_b1.Notes.IsPlainTextOnly Then s_Warn = s_Warn & T_81
			End If
		End If
		' Validate project data on this branch
		If b_ValidateProject Then
			' Check for relationships to Task summaries; same check whether this is a task or a summary
			's_1 = T_96a ' warning about links to task summaries
			s_2 = T_96b ' warning about bidirectional links
			s_3 = T_96c ' warning about multiple connections
			If i_DoRelationships > 0 Then ' relationships are in use
				For i_1 = 1 To m_b1.Relationships(True).Count
					Set r_1 = m_b1.Relationships(True).Item(i_1)
					Set m_b2 = f_OtherEnd(m_b1, r_1)
					If f_IsInTree(m_b1, m_wkgTopic) Then ' relationship inside project tree
						'If m_b1.Count > 0 Or m_b2.Count > 0 Then ' at least one is a task summary
						'	s_Warn = s_Warn & s_1 ' branch relationship to task summary
						'	s_1 = "" ' only warn once per branch
						'End If
						If f_IsTwoWay(r_1) Then
							s_Warn = s_Warn & s_2 ' warn about two-way relationships
							s_2 = "" ' only warn once per branch
						End If
						If f_IsLinked(m_b1, m_b2) > 1 Then ' connected more than once
							s_Warn = s_Warn & Replace(s_3, "%1", f_CleanBranchName(m_b2, False)) ' warn about multiple connections
							s_3 = "" ' only warn once
						End If
					End If
				Next i_1
				Set m_b2 = Nothing
				Set r_1 = Nothing
			End If
			' Check start date is not too early
			If f_HasStartDate(m_b1) Then
				If i_Vstart <> 0 Then
					If DateDiff("d", m_b1.Task.StartDate, i_Vstart) > 0 Then ' [1-001002] ".StartDate" : See "How to find all branch properties"
						s_Warn = s_Warn & Replace(Replace(T_75, "%1", Format(m_b1.Task.StartDate, "ddddd")), "%2", Format(i_Vstart, "ddddd")) ' [1-001002] ".StartDate" : See "How to find all branch properties"
					End If
				End If
				i_Vstart = m_b1.Task.StartDate ' carry validation start date forwards, [1-001002] ".StartDate" : See "How to find all branch properties"
			End If
			' Check due date is not too late
			If f_HasDeadlineDate(m_b1) Then
				If i_Vfinish <> 0 Then ' there is a validation finish date
					If DateDiff("d", m_b1.Task.DueDate, i_Vfinish) < 0 Then ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"
						s_Warn = s_Warn & Replace(Replace(T_76, "%1", Format(m_b1.Task.DueDate, "ddddd")), "%2", Format(i_Vfinish, "ddddd")) ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"
					End If
				End If
				i_Vfinish = m_b1.Task.DueDate ' carry validation finish date forwards, [1-001006] ".DeadlineDate" : See "How to find all branch properties"
			End If
			If (m_b1.SubTopics(False).Count > 0) Then ' This is a task summary, [6-000960] ".Count" : Ambiguous property. Check object type.
				If (m_b1.Task.DurationInHours > 0) Then s_Warn = s_Warn & T_74 ' Non-zero duration on Task summary, [1-000998] ".Duration" : See "How to find all branch properties"
			Else ' This is a task
				' Check for a duration on this task
				If m_b1.Task.DurationInHours = 0 Then ' [1-000998] ".Duration" : See "How to find all branch properties"
					' Check for % complete but no duration
					If (m_b1.Task.Complete >= 0) Then s_Warn = s_Warn & T_78 ' % complete >= 0, Duration = 0, [1-001058] ".PercentageDone" : See "How to find all branch properties"
					If (Not b_UseMilestoneCode) Or (b_UseMilestoneCode And (Not m_b1.Icons(True).HasStockIcon(mmStockIconClock) )) Then
						s_Warn = s_Warn & Replace(T_77, "%1", CStr(o_Project1.HoursPerDay)) ' No duration on Task
					End If
				Else ' there is a duration present
					If b_UseMilestoneCode And m_b1.Icons(True).HasStockIcon(mmStockIconClock) Then
						s_Warn = s_Warn & T_79 ' invalid milestone code on task with duration
					End If
					' Check for a Resource
					If Len(Trim(m_b1.Task.Resources)) = 0 And Len(s_DefaultResource) = 0 Then ' [1-001110] ".Resources" : See "How to find all branch properties"
						s_Warn = s_Warn & T_73 ' no resource specified on task
					End If
				End If
			End If
		End If
	End If ' end of branch processing

	' Report a message if necessary
	b_Continue = True ' assume no warnings
	If Len(s_Warn) > 0 Then b_Continue = f_IssueWarning(m_b1, s_Warn)

	' Now check all children
	For i_1 = 1 To m_b1.SubTopics(False).Count ' [6-000960] ".Count" : Ambiguous property. Check object type.
		If b_Continue Then b_Continue = f_ValidateBranch(m_b1.SubTopics(False).Item(i_1), i_level + 1, i_Vstart, i_Vfinish) ' [6-000922] ".Item" : Ambiguous property. Check object type.
		If Not b_Continue Then Exit For ' leave loop
	Next i_1

	f_ValidateBranch = b_Continue ' return result

End Function

 ' Function f_IssueWarning(ByVal m_b1 As Branch, ByVal s_Warn As String) As Boolean
Function f_IssueWarning(ByVal m_b1 As Topic, ByVal s_Warn As String) As Boolean ' [1-003372] "As Branch" : Branch becomes Topic
' Issue a warning message to the user and ask them if they want to continue
' Returns True if they do, False if they want to abandon
Dim s_1 As String

	m_b1.SelectOnly ' Select problem branch, [4-000966] ".Select" : Ambiguous property. Check object type.
	If m_b1.IsCentralTopic Then
		s_1 = T_70b ' "Central Topic"
	Else
		s_1 = Replace(T_70a, "%1", f_CleanBranchName(m_b1, False)) ' "Branch "x""
	End If
	f_IssueWarning = (MsgBox(Replace(Replace(T_70 & T_99, "%1", s_1), "%2", s_Warn), _
						vbExclamation + vbOkCancel, s_AppName) = vbOK)

End Function

 ' Function f_HasStartDate(ByVal m_b1 As Branch) As Boolean
Function f_HasStartDate(ByVal m_b1 As Topic) As Boolean ' [1-003372] "As Branch" : Branch becomes Topic
' Check whether a branch has a Start Date specified

	f_HasStartDate = (m_b1.Task.StartDate <> 0) ' [1-001002] ".StartDate" : See "How to find all branch properties"

End Function

 ' Function f_HasDeadlineDate(ByVal m_b1 As Branch) As Boolean
Function f_HasDeadlineDate(ByVal m_b1 As Topic) As Boolean ' [1-003372] "As Branch" : Branch becomes Topic
' Check whether a branch has a Deadline Date specified

	f_HasDeadlineDate = (m_b1.Task.DueDate <> 0) ' [1-001006] ".DeadlineDate" : See "How to find all branch properties"

End Function

Function f_HashGuid(ByVal s_Guid As String) As Long
' Return a consistent numeric hash of a Guid string
' Allows Guid to continue to be used as a numeric value
Dim i_Hash As Long
Dim i_1 As Long

	i_Hash = Len(s_Guid) ' seed value
	For i_1 = 1 To Len(s_Guid)
		i_Hash = i_Hash + (i_1 * Asc(Mid(s_Guid, i_1, 1))) ' build hash
	Next i_1
	f_HashGuid = i_Hash ' return result

End Function

Function f_DefaultPath() As String
' Return default path for exported or imported documents
' If the active document has been saved then use the path that it lives in
' Else use the default docs path for MindManager

	f_DefaultPath = f_Folderise(Path(mmDirectoryMyMaps)) ' read default documents folder
	If Documents.Count > 0 Then ' there is a document open
		If ActiveDocument.Name <> ActiveDocument.FullName Then ' saved document open
			f_DefaultPath = f_Folderise(ActiveDocument.Path) ' default to the path for this document
		End If
	End If

End Function

Function f_Folderise(ByVal s_path As String) As String
' Append trailing backslash to folder if required
Const s_slash = "\" ' assumed non-alphabetic

	s_path = Trim(s_path)
	If Right(s_path, Len(s_slash)) <> s_slash Then s_path = s_path & s_slash
	f_Folderise = s_path

End Function

 ' Function f_CleanBranchName(ByVal m_b1 As Branch, ByVal b_ForFilename As Boolean) As String
Function f_CleanBranchName(ByVal m_b1 As Topic, ByVal b_ForFilename As Boolean) As String ' [1-003372] "As Branch" : Branch becomes Topic
' Get trimmed branch name with wrapping removed
' If b_ForFilename is set then invalid filename characters are also removed
Dim i_1 As Integer
Dim s_Text As String

	s_Text = Replace(m_b1.Text, vbCrLf, vbLf) ' strip wrapping
	s_Text = Replace(s_Text, vbLf, " ")
	s_Text = Replace(s_Text, "  ", " ")
	If b_ForFilename Then ' remove any illegal filename characters
		For i_1 = 1 To Len(s_Badchars)
			s_Text = Replace(s_Text, Mid(s_Badchars, i_1, 1), "")
		Next i_1
	End If
	s_Text = Trim(s_Text)
	If Len(s_Text) = 0 Then s_Text = T_4 ' "Untitled" - handle unnamed branches
	f_CleanBranchName = s_Text

End Function

Function f_cropFolder(ByVal s_Filepath As String) As String
' Crop the folder and \ off a full filename
' It can then be used as a local filename
Dim i_1 As Integer

	f_cropFolder = s_Filepath ' default unchanged
	i_1 = InStrRev(s_Filepath, "\") ' look for last \ character
	If IsNull(i_1) Then Exit Function ' not found
	If i_1 < 1 Then Exit Function ' not found
	f_cropFolder = Right(s_Filepath, Len(s_Filepath) - i_1)

End Function

Function f_cropFile(ByVal s_Filepath As String) As String
' Crop the filename off a full path+filename
' It can then be used as a foldername (has trailing \ on it)
Dim i_1 As Integer

	f_cropFile = s_Filepath ' default unchanged
	i_1 = InStrRev(s_Filepath, "\") ' look for last \ character
	If IsNull(i_1) Then Exit Function ' not found
	f_cropFile = Left(s_Filepath, i_1)

End Function

Function f_IsAlpha(ByVal s_Chr As String) As Boolean
' Return true if first character of s_Chr is alphabetic
Dim i_chr As Integer

	i_chr = Asc(UCase(Left(s_Chr, 1)))
	f_IsAlpha = (i_chr >= Asc("A")) And (i_chr <= Asc("Z"))

End Function

Function f_OpenDoc(ByVal s_File As String) As Boolean
' Open a document with its registered application
Dim i_hWnd As Long ' New process handle - note this is not a window handle

	i_hWnd = ShellExecute(GetFocus(), "Open", s_File, "", "", 5) ' open file
	f_OpenDoc = (i_hWnd > 32) ' successfully launched

End Function

Sub p_writeFile(ByVal s_Filename As String, ByRef s_Content As String)
' Open and write the specified file

	s_Filename = Trim(s_Filename)
	If s_Filename = "" Then	Err.Raise(1000,,T_29,,) ' Filename missing
	Open s_Filename For Output As #1
	Print #1, s_Content; ' no newline at end, Scitor PS8 gets upset
	Close #1

End Sub

Sub p_DisplayError(ByVal b_Details As Boolean)
' Display the current error message with full details if requested
' Clear any error on completion
Dim s_details As String ' details of error, if requested

	Beep
	' Create error details if requested
	s_details =  T_21
	If b_Details Then s_details = s_details & vbCrLf & _
					"Err.Number: " & Err.Number & vbCrLf & _
					"Err.Source: " & Err.Source & vbCrLf & _
					"Err.Description: "
	s_details = s_details & Err.Description
	Err.Clear ' finished with error

	' Display error message to user, ask if they want to view the Help again
	Begin Dialog UserDialog 330,112,s_AppName ' %GRID:10,7,1,1
		Text 20,14,290,56,s_details,.Text1,2
		'PushButton 50,84,100,21,T_10,.PushButton1
		OKButton 110,84,100,21
	End Dialog
	Dim dlg_Error As UserDialog
	
	Select Case Dialog(dlg_Error)
		Case 1 ' Help
		'	If Len(s_HelpFile) > 0 Then Call f_OpenDoc(MacroDir & "\" & s_HelpFile)
  		'	If Len(s_HelpMap) > 0 Then Documents.Add(MacroDir & "\" & s_HelpMap).Activate ' open Help map
		'	If Len(s_HelpMap) > 0 Then Documents.Open(MacroDir & "\" & s_HelpMap, "", True).Activate ' open Help map, [3-001617] ".Add" : Ambiguous property. Check object type.; See "How to listen to events"
	End Select

End Sub

Function f_CheckMMVersion(ByVal s_minVersion As String, _
				ByVal s_MaxVersionMask As String) As Variant
' s_minVersion must be an exact version (lowest tested), e.g. "4.0.68"
' s_MaxVersionMask can be an exact version or a mask using X, e.g. "4.0.XX"
' Individal digits are not masked, i.e. "x" = "xx" = no limit
' Check MindManager version against declared range
' Returns vbOK if version is tested
' Returns user's choice of vbOK, vbCancel or vbAbort if not tested
Dim s_This As String
Dim s_1 As String
Dim v_Mins(1 To i_Parts) As Variant ' Minimum version numbers
Dim v_Maxes(1 To i_Parts) As Variant ' Maximum version numbers
Dim i_1 As Integer ' counter

	s_This = s_forcedVersion ' use a forced version if required
	If s_This = "" Then s_This = f_getMMVersion() ' read actual version if not forced
	' Parse current, min and max versions
	ReDim v_MMVersions(1 To i_Parts) ' set up array
	Call p_parseVersion(s_This, v_MMVersions()) ' parse into global array for external use
	Call p_parseVersion(s_minVersion, v_Mins())
	Call p_parseVersion(s_MaxVersionMask, v_Maxes())

	' Test this version against requirements for this utility
	f_CheckMMVersion = vbOK ' assume OK
	For i_1 = 1 To i_Parts
		If v_Mins(i_1) < 0 Then v_Mins(i_1) = v_MMVersions(i_1) ' auto pass min
		If v_MMVersions(i_1) < v_Mins(i_1) Then Exit For ' below minimum, fail
		If v_Maxes(i_1) < 0 Then v_Maxes(i_1) = v_MMVersions(i_1) ' auto pass max
		If v_MMVersions(i_1) > v_Maxes(i_1) Then Exit For ' above maximum, fail
		If v_MMVersions(i_1) > v_Mins(i_1) Then Exit Function ' in range, pass
		If i_1 = i_Parts Then Exit Function ' in range at lowest level, pass
	Next i_1
	' Not supported; has user already declined the warning?
	If GetSetting(s_AppName, s_KeySection, s_KeyWarn, "") = s_This Then Exit Function

	' This version of MindManager is not supported
	Dim s_warning As String ' warning message to display
	Dim i_userResponse As Integer ' user's choice

	Begin Dialog UserDialog 400,168,s_AppName' %GRID:10,7,1,1
		PushButton 10,140,100,21,T_952,.PushButton1
		PushButton 150,140,100,21,T_953,.PushButton2
		PushButton 290,140,100,21,T_954,.PushButton3
		TextBox 10,7,380,105,.warning,1
		CheckBox 90,119,230,14,T_955,.dontWarnMeAbout
	End Dialog
	Dim dlg_Warning As UserDialog

	' Display the splash dialog
	s_1 = T_950 & vbCrLf & vbCrLf & T_951
	s_1 = Replace(s_1, "%1", s_AppName)
	s_1 = Replace(s_1, "%2", s_minVersion)
	s_1 = Replace(s_1, "%3", s_MaxVersionMask)
	s_1 = Replace(s_1, "%4", s_This)
	dlg_Warning.warning = s_1
	i_userResponse = Dialog(dlg_Warning) ' display splash dialog
	Select Case i_userResponse ' parse user choice from here
		Case 1 ' display help
			' Display the help file
			If Len(s_HelpFile) > 0 Then Call f_OpenDoc(MacroDir & "\" & s_HelpFile)
			If Len(s_HelpMap) > 0 Then Documents.Open(MacroDir & "\" & s_HelpMap, "", True).Activate ' open Help map, [3-001617] ".Add" : Ambiguous property. Check object type.; See "How to listen to events"
			f_CheckMMVersion = vbAbort ' stop this MMScript
		Case 2 ' continue
			If dlg_Warning.dontWarnMeAbout > 0 Then
				' Save this MM version to suppress future warnings
				SaveSetting(s_AppName, s_KeySection, s_KeyWarn, s_This)
			End If
			f_CheckMMVersion = vbOK
		Case Else ' cancel
			f_CheckMMVersion = vbCancel
	End Select

End Function

Sub p_parseVersion(ByVal s_Version As String, v_vList() As Variant)
' Parse the version into the variant list
Dim i_1 As Integer

	' Prefix with non-numeric string and post-fix with default data in case format changes
	s_Version = " " & UCase(Replace(s_Version, s_SepChr, " ")) & " 0 0 0 0 0 " ' few spare
	For i_1 = 1 To i_Parts ' parse all parts
		v_vList(i_1) = 0
		While ((Not IsNumeric(Left(s_Version,1))) And (Left(s_Version,1) <> UCase(s_MaskChr))) _
				And (s_Version <> "")
			s_Version = Right(s_Version, Len(s_Version)-1) ' discard non-numerics and non mask chrs
		Wend
		While ((IsNumeric(Left(s_Version,1))) Or (Left(s_Version,1) = UCase(s_MaskChr))) _
				And (s_Version <> "")
			If Left(s_Version,1) = UCase(s_MaskChr) Then ' mask character
				v_vList(i_1) = -1 ' always no limit, no single digit masks
			Else
				v_vList(i_1) = (v_vList(i_1) * 10) + CInt(Left(s_Version,1))
			End If
			s_Version = Right(s_Version, Len(s_Version)-1)
		Wend
	Next i_1

End Sub

Sub p_ReadPrefs(ByVal m_Doc As Document)
Dim s_PID As String ' user property name
Dim i_PID As Long ' user property ID
Dim i_1 As Long ' pointer
Dim s_Prefs As String
Dim s_1 As String
Dim b_1 As Boolean ' temp flag
Dim custom As CustomAttributes

	' Look up the preferences
	s_Prefs = "" ' assume none
	' Try per-map preferences first
	If b_PermapPrefs Then
		If Not (m_Doc Is Nothing) Then
			's_PID = s_Owner & "." & s_AppName & "." & s_KeyPrefs
			Set custom = m_Doc.CentralTopic.Attributes("http://schemas.mindjet.com/MindManager/mpx/2.0")
			s_Prefs = custom.GetAttributeValue("default") ' read prefs list from this doc, [2-001026] ".GetUserProperty" : See: "How to create and use User Properties"; See: "How to create and use User Properties", [1-000390] ".MapTitle" : This is now a Property.
		End If
	End If

	' Try Registry prefs if none read from document
	If Len(s_Prefs) = 0 And b_RegistryPrefs Then
		s_Prefs = GetSetting(s_AppName, s_KeySection, s_KeyPrefs, "")
	End If
	If b_Debug Then Debug.Print "Preferences: " & s_Prefs
	If Len(s_Prefs) = 0 Then Exit Sub ' no preferences found, revert to defaults

	' Parse the preferences list into global variables
	i_1 = 1 ' start at beginning of preferences
	Call f_ReadListItem(s_Prefs, i_1, b_ScheduleFromStart)
	Call f_ReadListItem(s_Prefs, i_1, b_ValidateProject)
	Call f_ReadListItem(s_Prefs, i_1, b_CheckExportable)
	Call f_ReadListItem(s_Prefs, i_1, i_WorkingDaysPerWeek)
	Call f_ReadListItem(s_Prefs, i_1, i_DefaultPriority)
	Call f_ReadListItem(s_Prefs, i_1, s_DefaultResource)
	Call f_ReadListItem(s_Prefs, i_1, i_DoRelationships)
	Call f_ReadListItem(s_Prefs, i_1, b_IncludeProjectRollup)
	Call f_ReadListItem(s_Prefs, i_1, b_UseMilestoneCode)
	Call f_ReadListItem(s_Prefs, i_1, i_ImportSchedule)
	Call f_ReadListItem(s_Prefs, i_1, i_DefaultRelationship)
	Call f_ReadListItem(s_Prefs, i_1, i_HoursPerDay)

End Sub

Function f_ReadListItem(ByRef s_List As String, ByRef i_Ptr As Long, ByRef v_Value As Variant) As Boolean
' Read next item from the list, assign it to the value, advance pointers and return True if value read
' Else return False if end of list reached
Dim i_1 As Integer ' next value length
Dim s_Value As String ' value string

	i_1 = Val(Mid(s_List, i_Ptr, i_MaxPrefLen)) ' read next char count: allow to error if incorrect
	i_Ptr = i_Ptr + i_MaxPrefLen ' advance pointer
	If i_1 > 0 Then ' there is a value to read
		s_Value = Mid(s_List, i_Ptr, i_1) ' read this many characters
		i_Ptr = i_Ptr + i_1 ' advance pointer
		f_ReadListItem = True ' return true
	Else
		s_Value = "" ' no string
		f_ReadListItem = False ' return false
	End If
	' Assign value, depending on type
	On Error Resume Next ' ignore conversion errors and continue
	Select Case VarType(v_Value)
		Case vbString ' string value
			v_Value = s_Value ' assign string directly
		Case vbBoolean ' boolean
			v_Value = CBool(s_Value) ' convert and assign, if valid
		Case vbInteger
			v_Value = CInt(s_Value) ' convert and assign, if valid
		Case Else ' probably a long value
			If IsNumeric(s_Value) Then v_Value = CLng(s_Value) ' assign numeric value
	End Select
	Err.Clear ' clear any conversion errors
	On Error GoTo 0 ' restore error handling

End Function

Sub p_SavePrefs(ByVal m_Doc As Document)
' Write the options back to storage
Dim s_PID As String ' user property name
Dim i_PID As Long ' user property ID
Dim s_Prefs As String
Dim s_1 As String

	' Build preferences list
	s_Prefs = ""
	Call p_WriteListItem(b_ScheduleFromStart, s_Prefs)
	Call p_WriteListItem(b_ValidateProject, s_Prefs)
	Call p_WriteListItem(b_CheckExportable, s_Prefs)
	Call p_WriteListItem(i_WorkingDaysPerWeek, s_Prefs)
	Call p_WriteListItem(i_DefaultPriority, s_Prefs)
	Call p_WriteListItem(s_DefaultResource, s_Prefs)
	Call p_WriteListItem(i_DoRelationships, s_Prefs)
	Call p_WriteListItem(b_IncludeProjectRollup, s_Prefs)
	Call p_WriteListItem(b_UseMilestoneCode, s_Prefs)
	Call p_WriteListItem(i_ImportSchedule, s_Prefs)
	Call p_WriteListItem(i_DefaultRelationship, s_Prefs)
	Call p_WriteListItem(i_HoursPerDay, s_Prefs)

	' write zero length string to end
	Call p_WriteListItem("", s_Prefs)

	' Write preferences to storage
	If b_PermapPrefs Then
		If Not (m_Doc Is Nothing) Then
			's_PID = s_Owner & "." & s_AppName & "." & s_KeyPrefs
			m_Doc.CentralTopic.Attributes("http://schemas.mindjet.com/MindManager/mpx/2.0").SetAttributeValue("default", s_Prefs)
		End If
	End If

	' Save backup copy in Registry
	If b_RegistryPrefs Then
		SaveSetting(s_AppName, s_KeySection, s_KeyPrefs, s_Prefs)
	End If

End Sub

Sub p_WriteListItem(ByVal v_Item As Variant, ByRef s_Prefs As String)
' Write an item to the Preferences list
' Note that Text to Boolean conversion can fail on other language systems,
' so always store Booleans in numeric form
Dim s_1 As String

	If VarType(v_Item) = vbBoolean Then v_Item = CInt(v_Item) ' convert Booleans to a number
	s_1 = CStr(v_Item) ' convert to string
	If Len(CStr(Len(s_1))) > i_MaxPrefLen Then Err.Raise(1000, , T_20, ,) ' Preference setting too long
	s_Prefs = s_Prefs & Format(Len(s_1), String(i_MaxPrefLen, "0")) & s_1 ' append to string

End Sub

Sub p_Dump(Optional ByVal s_File)
' Debug routine: Dump a file if specified, or the Project object if no filename given
Dim s_Title As String
Dim s_Content As String
Dim s_1 As String

	If IsMissing(s_File) Then ' dump project
		s_Title = "Project Object"
		s_Content = o_Project1.Dump ' dump the project object
	Else ' dump a file
		s_Title = f_cropFolder(s_File)
		s_Content = ""
		Open s_File For Input As #1
		While Not EOF(1)
			Line Input #1, s_1
			s_Content = s_Content & s_1 & vbCrLf
		Wend
		Close #1
	End If

	Begin Dialog UserDialog 750,483,s_AppName' %GRID:10,7,1,1
		Text 10,7,200,14,s_Title,.Title
		TextBox 10,21,730,427,.Content,1
		OKButton 300,455,140,21
	End Dialog
	Dim dlg_Dump As UserDialog
	dlg_Dump.Content = s_Content ' load contents
	Dialog dlg_Dump

End Sub

Sub p_SetWinBuf(ByRef s_ptr)
' Set up the Windows buffer for use by API calls

	s_ptr = String(100, " ") ' space for 100 characters

End Sub

Function f_ReadWinBuf(ByVal s_buf) As String
' Read contents of Windows buffer after a call to an API function

	f_ReadWinBuf = ""
	If Len(s_buf) = 0 Then Exit Function
	f_ReadWinBuf = Left(s_buf, InStr(s_buf, Chr(0)) - 1) ' look for first 0 character

End Function

Function f_getMMVersion() As String
' Read MindManager version
' Trim extra crud off introduced in 4.6

	f_getMMVersion = Trim(Replace(Replace(Me.Version, vbCr, ""), vbLf, ""))

End Function

Function f_Split(ByVal s_list As String, ByVal s_sep As String) As Variant
' Longhand split() function as some versions of MMScript don't like regular Split()
Dim i_1 As Long
Dim i_2 As Long
Dim s_Array() As String
Dim i_Count As Integer ' number of elements in array

	Erase s_Array ' clear array
	i_Count = 0 ' array is empty
	i_1 = 1 ' start at first char
	If Len(s_list) > 0 Then ' there is some data
		If Len(s_sep) > 0 Then ' there is a separator character
			While True
				i_2 = InStr(i_1, s_list, s_sep) ' find next separator
				If i_2 > 0 Then ' found one
					ReDim Preserve s_Array(0 To i_Count)
					s_Array(i_Count) = Mid(s_list, i_1, (i_2 - i_1))
					i_Count = i_Count + 1 ' count entry
					i_1 = i_2 + Len(s_sep) ' move pointer forwards for next search
				Else ' not found, return runt
					Exit While
				End If
			Wend
		End If
	End If
	' return remainder in final entry
	ReDim Preserve s_Array(0 To i_Count)
	s_Array(i_Count) = Right(s_list, (Len(s_list) - i_1) + 1)
	f_Split = s_Array ' return array as result

End Function

Function f_Join(ByRef s_Array() As String, ByVal s_sep As String) As String
' Longhand Join() function to go with f_Split()
Dim i_1 As Integer
Dim s_Return As String

	s_Return = "" ' default result, even if array is null
	' check array is initialised and has at least one entry
	On Error Resume Next
	i_1 = LBound(s_Array) ' read lower array bound
	If Err.Number <> 0 Then ' an error occurred; array is not initialised
		Err.Clear ' skip error
		On Error GoTo 0 ' restore error handling
	Else ' no error, continue
		On Error GoTo 0 ' restore error handling
		For i_1 = LBound(s_Array) To UBound(s_Array)
			s_Return = s_Return & s_Array(i_1)
			If i_1 < UBound(s_Array) Then s_Return = s_Return & s_sep ' more to follow, add join char
		Next i_1
	End If
	f_Join = s_Return ' return result

End Function
