	PROGRAM STRETCH
C               =======
C
C o The program is used to analyze stretch data file logs.  It takes a
C   command line as follows:
C
C   Parameter P1:	A list of stretch data input files.
C			Wildcards and lists are permitted.
C
C   /AVERAGE=delta-time Stretch file entries are taken modulo the given
C			delta-time (after subtracting the earliest time),
C			and averaged together.  The graph thus shows each
C			bar labelled with a delta time, instead of an
C			absolute time.  This is useful in generating,
C			for example, a graph of an "average" day or week.
C
C   /BEFORE=date-time	Select only entries before the indicated date/time.
C
C   /[NO]BIAS		Subtract out the smallest stretch time encountered
C			in any file before graphing.  The bias is computed
C			for each file separately if /STACK is specified.
C
C   /DAYS=(d,d-d,...)	One or more days to be included in the analysis.
C			The default (no /DAYS) is to include all days.
C			If /DAYS is specified, then only those data for
C			those days specified will be analyzed.  Also
C			graph bars falling entirely within non-specified
C			days will not be plotted.
C
C   /HOURS=(n,n-n,...)	A list of hours of the day to be graphed.  Entries
C			falling outside the specified hours are ignored,
C			and graph points at hours not selected are not shown.
C			The default is all hours.  Hours may be specified
C			as a list consisting of any mixture of single
C			hour numbers (0-23) or a range of hours of the form
C			"low-high".
C
C   /INTERVAL=delta-time
C			Average all entries within the given delta-time
C			of each other and display as a single point.  The
C			/SINCE date is used as the beginning of the initial
C			interval, if specified, otherwise, the first data
C			point time becomes the base interval value.  The
C			default is /INTERVAL=1:00:00.00
C
C   /[NO]LOG		Log each file as it is processed
C
C   /MAXIMUM=value	Specify max. value (floating point) for graph scale.
C			The actual max. value used will be the next higher
C			multiple of 1, 2 or 5.
C
C   /[NO]NORMALIZE	/NORMALIZE causes data values to be scaled according
C			to the smallest stretch time found.  This causes
C			the values to range upwards from 1.  This is the
C			default.  /NONORMALIZE causes the unscaled values
C			to be plotted.  If /STACK is specified, then each
C			file's samples are normalized separately.
C
C   /OUTPUT=filename	The name of the file for the graph(s).
C			The default is SYS$OUTPUT.
C
C   /PAGE=n		The number of lines on an output graph page.
C			The default is 60.
C
C   /SINCE=date-time	Select only entries since the indicated date/time.
C
C   /STACK[=(name,...)] Stack bars from consecutive input files,
C			instead of an average over all files.  The names
C			are the legends of the bars in each stack.  If
C		        the name for a file is blank (missing), then
C		        the filename component of the file is used.
C			Stack names are limited to 20 characters.
C
C   /[NO]SQUEEZE	[Don't] plot bars for which there is no data.
C
C   /TITLE=string	A title for the graph(s)
C
C S. Duff - Jan. 1986
C
C Revision History
C ================
C o 17-Jul-1986 [SGD]:
C   Fix /SQUEEZE with /DAYS (SELECT_DAY_HOUR code had mucho bugs)
C
C o 03-Jun-1986 [SDD]:
C   Add /DAYS
C
C o 12-May-1986 [SGD]:
C   Add X-axis label
C
C o 07-Apr-1986 [SGD]:
C   Add /STACK.  Fix normalization for /STACK.
C
C o 19-Feb-1986 [SGD]:
C   Use smallest sample for /NORMALIZATION.  Previously, was using smallest
C   >>averaged<< bucket.  This change means that the smallest bar in a
C   /NORMALIZE graph may be larger than unity if the bucket that contains
C   the smallest sample contains other samples as well.  This change provides
C   a more accurate picture of normalized performance.
C
C o 18-Feb-1986 [SGD]:
C   Fix recognition of since/before in check_range
C
	Implicit None
	Include 'Stretch.Inc/List'
	Include '($Rmsdef)/Nolist'

C	Codes

	External Stretch_nocmd, Stretch_inspecerr
	External Cli$_absent, Str$parse_table

C	Functions

	Integer Cli$dcl_parse, Cli$get_value
	Integer Lib$get_foreign, Lib$cvt_atime, Lib$cvt_dtime
	Integer Lib$find_file

C	Local store

	Integer Got_file, Got_spec, Istat, Context, Position, Lastposition
	Character*200 Infile, Infile_spec

C	Get and parse command line

	Istat = Lib$get_foreign(Str_cmd_line, 'Stretch> ')
	If (.Not. Istat) Call Lib$stop(Stretch_nocmd,,%Val(Istat))

	Istat = Cli$dcl_parse('STRETCH '//Str_cmd_line, Str$parse_table)
	If (.Not. Istat) Call Lib$stop(Stretch_nocmd,,%Val(Istat))

C	Initialize earliest and latest times to max and min date/time
C	so they will be set on the first valid stretch record

	Str_earliest(1) = 'FFFFFFFF'X
	Str_earliest(2) = '7FFFFFFF'X
	Str_latest(1) = 0
	Str_latest(2) = 0

C	Clear the buckets

	Call Str$clear_buckets

C	Get the qualifier values

	Call Str$get_quals

C	Open the output (graph) file

	Open(Unit = Str$k_outfile_unit, Status = 'New',
     $		  Carriagecontrol = 'List', Access = 'Sequential',
     $		  Name = Str_outfile, Recl = Str$k_outwidth)
	Str_linenum = 0
	Str_pagenum = 0
	Str_graphing = .False.

C	Loop across input file specs

	Str_stackcnt = 0
	Got_spec = Cli$get_value('P1',Infile_spec)
	Do While(Got_spec)

C	  Loop across files for this spec

	  Context = 0
	  Got_file = Lib$find_file(Infile_spec, Infile, Context,,,, 2)
	  Do While(Got_file)

C	    Open this file

	    Open(Unit = Str$k_infile_unit, Status = 'Old',
     $		  Form = 'Formatted', Access = 'Sequential',
     $		  Name = Infile)

C	    Set stack count

	    If (Str_stack) Str_stackcnt = Str_stackcnt + 1
	    If (.Not. Str_stack) Str_stackcnt = 1

C	    Log it

	    If (Str_log) Call Str$print_line(
     $		      'Processing file: '//
     $		      Infile(1:Min(Len(Infile), Str$k_outwidth - 20)))

C	    If stacking and no stack title for this file, then use
C	    filename component as title

	    If (Str_stack .And. (Str_stack_title(Str_stackcnt) .Eq. ' ')) Then

C	      Find position of last "]" or ":" and following "." to
C	      delimit filename.

	      Position = 1
	      Lastposition = 0
	      Do While (Position .Ne. Lastposition)
	        Lastposition = Position
	        Position = Position + Index(Infile(Position+1:),']')
	        If (Position .Eq. Lastposition)
     $		      Position = Position + Index(Infile(Position+1:),':')
	      End Do
	      Position = Lastposition + Index(Infile(Lastposition+1:),'.')
	      If ((Position + 1) .Ge. (Lastposition - 1)) Then
	        Str_stack_title(Str_stackcnt) =
     $			      Infile(Lastposition+1:Position-1)
	        Call Str$trim(Str_stack_title(Str_stackcnt),
     $			      Str_stack_title(Str_stackcnt),
     $			      Str_stack_title_len(Str_stackcnt))
	        If (Str_stack_title_len(Str_stackcnt) .Le. 0)
     $			      Str_stack_title_len(Str_stackcnt) = 1
	      End If
	    End If

C	    Process this input file

	    Call Str$do_file

C	    Close this input file

	    Close(Unit = Str$k_infile_unit)

C	    Get next file

	    Got_file = Lib$find_file(Infile_spec, Infile, Context,,,, 2)
	  Enddo

C	  Check status

	  If (Got_file .Ne. Rms$_nmf)
     $	      Call Lib$stop(Stretch_inspecerr,, %Val(Got_file))

C	  Get next spec

	  Call Lib$find_file_end(Context)
	  Got_spec = Cli$get_value('P1',Infile_spec)
	Enddo

C	Check status

	If (Got_spec .Ne. %Loc(Cli$_absent))
     $	      Call Lib$stop(Stretch_inspecerr,,%Val(Got_spec))

C	Skip to new page

	If (Str_linenum .Gt. 0) Str_linenum = 99999999
	Call Str$print_bottom_page

C	Do the graph(s)

	Str_graphing = .True.
	Call Str$do_graph
	Str_graphing = .False.

C	Display the command line

	Call Str$print_line(' ')
	Call Str$print_line('Command line used: '//Str_cmd_line)
	End
	Logical Function Str$add_bucket(Time, Val)

C o This routine adds the stretch value VAL to the bucket represented by
C   the quadword TIME.  It fails if the time value is out of range.

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Functions

	Integer Qmath$compare

C	Parameters

	Integer Time(2), Val

C	Local store

	Integer This_bucket, Bucket(2), Time_offset(2), Room, Newroom

C	Assume failure

	Str$add_bucket = .False.

C	Figure the bucket for this time.  If first_bucket is negative,
C	then no start has been recorded yet.  (We set the time on the
C	first bucket to be the first multiple of the interval value
C	prior to the given time.

	If (Str_first_bucket .Lt. 0) Then
	  Str$add_bucket = .True.
	  Call Qmath$divide(Time, Str_interval, Time_offset)
	  Call Qmath$multiply(Time_offset, Str_interval, Time_offset)
	  Str_bucket_begin(1) = Time_offset(1)
	  Str_bucket_begin(2) = Time_offset(2)
	  Str_first_bucket = 0
	  Str_last_bucket = 0
	  This_bucket = 0

C	Not first value.

	Else

C	  Compute number of buckets unused (ROOM), and subtract bucket's
C	  start time from this time to get delta.

	  Room = Str_first_bucket - Str_last_bucket - 1
	  If (Room .Lt. 0) Room = Room + Str$k_max_buckets
	  Call Qmath$subtract(Time, Str_bucket_begin, Time_offset)

C	  If negative, then must move start time and beginning bucket back

	  If (Time_offset(2) .Lt. 0) Then
	    Call Qmath$subtract(Str_bucket_begin, Time, Time_offset)
	    Call Qmath$divide(Time_offset, Str_interval, Bucket)
	    If (Bucket(2) .Ne. 0) Return
	    This_bucket = Bucket(1) + 1
	    If (This_bucket .Gt. Room) Return
	    Call Qmath$multiply_int(Str_interval, This_bucket, Time_offset)
	    This_bucket = Str_first_bucket - This_bucket
	    If (This_bucket .Lt. 0)
     $		      This_bucket = This_bucket + Str$k_max_buckets
	    Str_first_bucket = This_bucket
	    Call Qmath$subtract(
     $		      Str_bucket_begin, Time_offset, Str_bucket_begin)

C	  Time is past starting time.  Compute bucket offset from current,
C	  and update last_bucket if necessary.

	  Else
	    Call Qmath$divide(Time_offset, Str_interval, Bucket)
	    If (Bucket(2) .Ne. 0) Return
	    If (Bucket(1) .Gt. Str$k_max_buckets) Return
	    This_bucket = Bucket(1) + Str_first_bucket
	    If (This_bucket .Ge. Str$k_max_buckets)
     $		      This_bucket = This_bucket - Str$k_max_buckets
	    Newroom = Str_first_bucket - This_bucket - 1
	    If (Newroom .Lt. 0) Newroom = Newroom + Str$k_max_buckets
	    If (Newroom .Lt. Room) Str_last_bucket = This_bucket
	  End If
	End If

C	Update earliest and latest

	If (Qmath$compare(Time, Str_earliest) .Lt. 0) Then
	  Str_earliest(1) = Time(1)
	  Str_earliest(2) = Time(2)
	End If
	If (Qmath$compare(Time, Str_latest) .Gt. 0) Then
	  Str_latest(1) = Time(1)
	  Str_latest(2) = Time(2)
	End If

C	Update bucket.

	Str$add_bucket = .True.
	Str_minval(Str_stackcnt) = Min(Str_minval(Str_stackcnt), Val)
	Str_buckets(Str_stackcnt,This_bucket) =
     $		      Str_buckets(Str_stackcnt,This_bucket) + Val
	Str_bucket_cnt(Str_stackcnt,This_bucket) =
     $		      Str_bucket_cnt(Str_stackcnt,This_bucket) + 1
	End
	Logical Function Str$check_day_hour(Time, Interval)

C o Check whether the given INTERVAL (beginning at "TIME") is hit by
C   at least one hour in STR_HOURS and at least one day in STR_DAYS.
C   Note that a bucket that is not in the hour or day range will have
C   no data, so this routine is only used if a bucket is empty, and
C   /NOSQUEEZE is specified, to discriminate between a no-data bucket
C   and an out-of-range bucket.

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Functions

	Integer Qmath$compare

C	Parameters

	Integer Time(2), Interval(2)

C	Local store

	Integer Day6(2) /0,0/, Hour23(2) /0,0/, Zeroq(2) /0,0/
	Integer This_time(2), Start_hour, End_hour, Ihour
	Integer Start_day, End_day, Iday
	Integer*2 Timbuf(7)

C	Assume success

	Str$check_day_hour = .True.

C	Initialize if required

	If (Hour23(2) .Eq. 0) Then
	  Call Sys$bintim('0 23:00:00.00', Hour23)
	  Call Sys$bintim('6 00:00:00.00', Day6)
	  Call Qmath$subtract(Zeroq, Hour23, Hour23)
	  Call Qmath$subtract(Zeroq, Day6, Day6)
	End If

C	If interval is more than 6 days, we have to hit something

	If (Qmath$compare(Interval, Day6) .Eq. 1) Return

C	If no days specified and more than 23 hours, must have at least one hour
C	in range.

  	If ((.Not. Str_day_qual) .And.
	1	    (Qmath$compare(Interval, Hour23) .Eq. 1)) Return

C	Now assume false

	Str$check_day_hour = .False.

C	Get starting hour

	Call Sys$numtim(Timbuf, Time)
	Start_hour = Timbuf(4)

C	Get ending hour

	Call Qmath$add(Time, Interval, This_time)
	Call Qmath$subtract_int(This_time, 1, This_time)
	Call Sys$numtim(Timbuf, This_time)
	End_hour = Timbuf(4)
	If (End_hour .Lt. Start_hour) End_hour = End_hour + 24

C	Loop to check hours

	Do Ihour = Start_hour, End_hour
	  If (Str_hours(Mod(Ihour,24))) Goto 100
	End Do

C	Must fail (no hour in range)

	Return

C	Hours checked out - check days

100	Continue
	Call Lib$day_of_week(Time, Start_day)
	Start_day = Start_day - 1

C	Get ending day

	Call Qmath$add(Time, Interval, This_time)
	Call Qmath$subtract_int(This_time, 1, This_time)
	Call Lib$day_of_week(This_time, End_day)
	End_day = End_day - 1
	If (End_day .Lt. Start_day) End_day = End_day + 7

C	Loop to check days.  If we hit one, then return

	Do Iday = Start_day, End_day
	  If (Str_days(Mod(Iday, 7))) Then
	    Str$check_day_hour = .True.
	    Return
	  End If
	End Do

C	Must fail (no hour in range)

	Return
	End
	Logical Function Str$check_range(Time)

C o Check whether the given quadword Time falls in the /SINCE, /BEFORE
C  /HOURS and /DAYS range.  Return .TRUE. iff it does.

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Functions

	Integer Qmath$compare

C	Parameters

	Integer Time(2)

C	Local Store

	Integer*2 Timbuf(7)
	Integer*2 Hour_of_day
	Integer Daynum
	Equivalence (Timbuf(4), Hour_of_day)

C	Assume failure

	Str$check_range = .False.

C	Check /BEFORE

	If (Str_before(2) .Ge. 0) Then
	  If (Qmath$compare(Time, Str_before) .Ne. -1) Return
	Endif

C	Check /SINCE

	If (Str_since(2) .Ge. 0) Then
	  If (Qmath$compare(Time, Str_since) .Eq. -1) Return
	Endif

C	Check /HOURS

	Call Sys$numtim(Timbuf, Time)
	If (.Not. Str_hours(Hour_of_day)) Return

C	Check /DAYS

	If (Str_day_qual) Then
	  Call Lib$day_of_week(Time, Daynum)
	  Daynum = Daynum - 1
	  If (.Not. Str_days(Daynum)) Return
	End If

C	Success

	Str$check_range = .True.
	End
	Subroutine Str$clear_buckets

C o Clear the bucket array in preparation for loading

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Local store

	Integer Ibucket, Istack

C	Initialize bucket pointers

	Str_first_bucket = -1
	Str_last_bucket = -1

C	Loop across stack sections

	Do Istack = 1, Str$k_max_stack

C	  Set minimum so far to max

	  Str_minval(Istack) = '7FFFFFFF'X

C	  Loop across buckets to initialize the count and amount

	  Do Ibucket = 0, Str$k_max_buckets
	    Str_buckets(Istack,Ibucket) = 0
	    Str_bucket_cnt(Istack,Ibucket) = 0
	  End Do
	End Do
	End
	Subroutine Str$do_file

C o This routine processes the data on a single input file

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Codes

	External Stretch_strvalerr, Stretch_strtimerr, Stretch_rangeerr

C	Functions

	Integer Ots$cvt_ti_l, Sys$bintim
	Logical Str$check_range, Str$add_bucket

C	Local store

	Character*23 Time_spec
	Character*10 Stretch_spec
	Integer Stretch_value, Time_value(2), Rec_count, Select_count
	Integer Istat
	Logical Inrange, Rangeerr
	Character*132 Line

C	Loop across all input records

	Rangeerr = .False.
	Rec_count = 0
	Select_count = 0
	Do While (.True.)
	  Read(Unit = Str$k_infile_unit, Fmt = '(A23,X,A10)', End = 1000)
     $		      Time_spec, Stretch_spec
	  Rec_count = Rec_count + 1
	  Istat = Sys$bintim(Time_spec, Time_value)
	  If (Istat) Then
	    If (Str$check_range(Time_value)) Then

C	      Convert stretch value to integer

	      Istat = Ots$cvt_ti_l(Stretch_spec, Stretch_value,
     $			  %Val(4), %Val('9'x))

C	      If conversion failed, then signal informational error

	      If ((.Not. Istat) .Or. (Stretch_value .Le. 0)) Then
		Call Lib$signal(Stretch_strvalerr, %Val(1), Stretch_spec,
     $			  %Val(Istat))

C	      Otherwise, add it to a bucket

	      Else
	        Inrange = Str$add_bucket(Time_value, Stretch_value)
	        If (Inrange) Then
		  Select_count = Select_count + 1
	        Else
		  If (.Not. Rangeerr) Then
		    Call Lib$signal(Stretch_rangeerr, %Val(1), Time_spec)
		    Rangeerr = .True.
	          End If
	        End If
	      End If
	    End If

C	  Invalid time specification in log

	  Else
	    Call Lib$signal(Stretch_strtimerr, %Val(1), Stretch_spec,
     $			  %Val(Istat))
	  End If
	End Do

C	Here when EOF on input file

1000	Continue
	If (Str_log) Then
	  Line = ' '
	  Call Sys$fao('!SL records read, !SL records selected.',, Line,
     $		      %Val(Rec_count), %Val(Select_count))
	  Call Str$print_line(Line)
	End If
	End
	Subroutine Str$do_graph

C o Called to perform graph generation/emission, after loading buckets

	Implicit None
	Include 'Stretch.inc/Nolist'

C	Codes

	External Stretch_nodata

C	Functions
	
	Integer Str$normalize_buckets

C	Local store

	Integer Ibucket, Nbucket, Scale, This_time(2), Occupied, Istack
	Real Bmin, Bmax, Limit, Increment

C	Setup the values in the buckets

	Occupied = Str$normalize_buckets(Bmin, Bmax)

C	Just return if no data

	If (Occupied .Le. 0) Then
	  Call Lib$signal(Stretch_nodata)
	  Return
	End If

C	Choose a nice multiple of 1,2 or 5 as the limit, based on the max
C	of the range.  Place in LIMIT.

	Scale = 1
	Limit = Bmax
	If (Str_max_present) Limit = Str_max

C	Scale down to a limit below 10

	Do While (Limit .Ge. 10.0)
	  Limit = Limit / 10.0
	  Scale = Scale * 10
	End Do

C	Scale up to a limit of at least 1

	Do While (Limit .Lt. 1.0)
	  Limit = Limit * 10.0
	  Scale = Scale / 10
	End Do

C	Now we have a limit in the single digit range.  Choose a nice
C	value just above it.

	If (Limit .Gt. 5.0) Then
	  Limit = 10.0
	Else If (Limit .Gt. 2.0) Then
	  Limit = 5.0
	Else If (Limit .Gt. 1.0) Then
	  Limit = 2.0
	Else
	  Limit = 1.0
	Endif
	Limit = Limit * Scale

C	Initialize for active bucket loop

	This_time(1) = Str_bucket_begin(1)
	This_time(2) = Str_bucket_begin(2)
	Increment = Limit / Str$k_barlen

C	Format graph strings

	Call Str$format_header(Increment)

C	Loop across the (contiguous) range of active buckets

	Do Ibucket = Str_first_bucket, Str_first_bucket + Occupied - 1

C	  Get bucket index

	  Nbucket = Ibucket
	  If (Nbucket .Ge. Str$k_max_buckets)
     $		      Nbucket = Nbucket - Str$k_max_buckets

C	  If doing stack, then check for not enough room for stack

	  If (Str_stack .And.
     $		  ((Str_pagelength - Str_linenum) .Le. Str_stackcnt)) Then
	    Str_linenum = 99999999
	    Call Str$print_bottom_page
	  End If

C	  Loop across stacked bars.

	  Do Istack = 1, Str_stackcnt

C	    Call to (possibly) print the line for this bucket

	    Call Str$print_bar(Nbucket, Istack, This_time, Increment)
	  End Do

C	  Bump date to next bucket

	  Call Qmath$add(Str_interval, This_time, This_time)
	End Do

C	Print a footer

	Call Str$print_bottom_page
	End
	Subroutine Str$print_line(Line)

C o Put a line to the log file with header/footer checks

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Parameters

	Character*(*) Line

C	Do bottom of page if required

	If (Str_linenum .Ge. Str_pagelength) Call Str$print_bottom_page

C	Do top of page if required

	If (Str_linenum .Eq. 0) Call Str$print_top_page

C	Print the line

	Call Str$emit_line(Line)
	End
	Subroutine Str$emit_line(Line)

C o Print a given string (LINE).  Called by Str$print_line, and directly
C   from print_top_page and print_bottom_page (to avoid recursion).

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Parameters

	Character*(*) Line

C	Local store

	Integer Ichunk, Linelen, Ichar
	Character*132 Outline

C	Trim the line (leave at least one character)

	Ichar = Len(Line)
	Do While((Ichar .Gt. 1) .And. (Line(Ichar:Ichar) .Eq. ' '))
	  Ichar = Ichar - 1
	End Do

C	Write the chunks of the line

	Do Ichunk = 1, Ichar, 132
	  Call Str$trim(
     $		      Outline, Line(Ichunk:Min(Ichunk + 131, Ichar)),
     $		      Linelen)
	  Write(Unit = Str$k_outfile_unit, Fmt = '(A)')
     $		      Outline(1:Max(1, Linelen))
	  Str_linenum = Str_linenum + 1
	End Do
	End
	Subroutine Str$format_header(Increment)

C o Setup graph string constants (called at the start of graphing).

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Parameters

	Real Increment

C	Local Store

	Integer Iline, Ndigits, Ndecimals
	Character*12 Format

C	Make "+---...---+" thing

	Do Iline = 1, Str$k_barlen, 10
	  Str_dashes(Iline:Iline + 9) = '+---------'
	End Do
	Str_dashes(Str$k_barlen+1:) = '+'

C	Make "|   |   |..." thing

	Str_divider = ' '
	Do Iline = 1, Str$k_barlen + 1, 10
	  Str_divider(Iline:Iline) = '|'
	End Do

C	Make "|****+****+****+...+" thing

	Str_stars(1:1) = '|'
	Do Iline = 2, Str$k_barlen + 1, 5
	  Str_stars(Iline:Iline + 4) = '****+'
	End Do

C	Make x-axis label.  This depends on the qualifiers.  We make
C	it centered so it looks nice.

	If (Str_normalize) Then
	  Str_xlabel = 'Relative Response Time'
	Else If (Str_bias) Then
	  Str_xlabel = 'Shifted Response Time in seconds (times 100)'
	Else
	  Str_xlabel = 'Response Time in seconds (times 100)'
	End If
	Call Str$trim(Str_xlabel,Str_xlabel,Str_xlabel_len)
	Str_xlabel_len = (Len(Str_xlabel) - Str_xlabel_len) / 2
	Str_xlabel(Str_xlabel_len:) = Str_xlabel
	Str_xlabel(1:Str_xlabel_len-1) = ' '
	Call Str$trim(Str_xlabel,Str_xlabel,Str_xlabel_len)

C	Now make header.  Eight places (max) are used for numeric labels
C	(at every division).  The number of decimals is computed based
C	on the increment we are given.

	Str_header = Str_divider
	If (Increment .Gt. 0.1) Then

C	  Insert the value label breaks

	  Do Iline = 2, Str$k_barlen + 3, 10
	    Call Sys$fao('!9<!SL!>',, Str_header(Iline:Iline + 8),
     $		      %Val(Int(Increment * (Iline - 2))))
	  End Do
	Else

C	  Insert the value label breaks

	  Do Iline = 2, Str$k_barlen + 3, 10
	    Write(Str_header(Iline:Iline + 8), Fmt = '(F6.3)')
     $		      Increment * (Iline - 2)
	  End Do
	End If
	End
	Subroutine Str$get_days

C o This routine loads the days array from the /DAYS qualifiers

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Codes

	External Stretch_dayerr
	External Cli$_absent

C	Functions

	Integer Cli$present, Cli$get_value

C	Local store

	Integer Istat, Iday, Got_spec, Low_day, High_day, Range_pos
	Character*16 Day_spec
	Character*21 Day_list
	1   /',MO,TU,WE,TH,FR,SA,SU'/

C	Set qualifier and initialize depending on presence

	Str_day_qual = Cli$present('DAYS')
	Do Iday = 0, 6
	  Str_days(Iday) = .Not. Str_day_qual
	End Do

C	Loop, processing all the day specs

	Got_spec = Cli$get_value('DAYS', Day_spec)
	Do While (Got_spec)
	  Low_day = Index(Day_list, ','//Day_spec(1:2))
	  If (Low_day .Eq. 0) Call Lib$stop(Stretch_dayerr)
	  Low_day = Low_day / 3
	  Range_pos = Index(Day_spec,'-')
	  If ((Range_pos .Eq. 1) .Or. (Range_pos .Eq. Len(Day_spec)))
     $		      Call Lib$stop(Stretch_dayerr)

C	  Check for single day specification

	  If (Range_pos .Eq. 0) Then
	    High_day = Low_day

C	  Have a range spec

	  Else
	    If ((Range_pos + 2) .Gt. Len(Day_spec))
	1		Call Lib$stop(Stretch_dayerr)
	    High_day = Index(
	1		Day_list, ','//Day_spec(Range_pos + 1: Range_pos + 2))
	    If (High_day .Eq. 0) Call Lib$stop(Stretch_dayerr)
	    High_day = High_day / 3
	  End If

C	  Loop across range to mark

	  If (High_day .Lt. Low_day) High_day = High_day + 7
	  Do Iday = Low_day, High_day
	    Str_days(Mod(Iday,7)) = .True.
	  End Do

C	  Get next spec

	  Got_spec = Cli$get_value('DAYS', Day_spec)
	End Do
	End
	Subroutine Str$get_hours

C o This routine loads the hours array from the /HOURS qualifier

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Codes

	External Stretch_hourserr
	External Cli$_absent

C	Functions

	Integer Cli$present, Cli$get_value, Ots$cvt_ti_l

C	Local store

	Integer Istat, Ihour, Got_spec, Low_hour, High_hour, Range_pos
	Character*5 Hour_spec

	Istat = Cli$present('HOURS')

C	If /HOURS not present, then select all hours

	If (.Not. Istat) Then
	  Do Ihour = 0, 23
	    Str_hours(Ihour) = .True.
	  End Do

C	Otherwise /HOURS is present

	Else

C	  Assume no hours

	  Do Ihour = 0, 23
	    Str_hours(Ihour) = .False.
	  End Do

C	  Loop, processing all the hour specs

	  Got_spec = Cli$get_value('HOURS',hour_spec)
	  Do While (Got_spec)
	    Range_pos = Index(Hour_spec,'-')
	    If ((Range_pos .Eq. 1) .Or. (Range_pos .Eq. Len(Hour_spec)))
     $		      Call Lib$stop(Stretch_hourserr)

C	    Check for single hour specification

	    If (Range_pos .Eq. 0) Then
	      Istat = Ots$cvt_ti_l(Hour_spec, Low_hour, %Val(4), %Val('9'x))
	      If (.Not. Istat) Call Lib$stop(Stretch_hourserr,, %Val(Istat))
	      High_hour = Low_hour

C	    Have a range spec

	    Else
	      Istat = Ots$cvt_ti_l(Hour_spec(1:Range_pos - 1), Low_hour,
     $			  %Val(4), %Val('9'x))
	      If (.Not. Istat) Call Lib$stop(Stretch_hourserr,, %Val(Istat))
	      Istat = Ots$cvt_ti_l(Hour_spec(Range_pos + 1:), High_hour,
     $			  %Val(4), %Val('9'x))
	      If (.Not. Istat) Call Lib$stop(Stretch_hourserr,, %Val(Istat))
	    End If

C	    Check validity of range

	    If ((Low_hour .Lt. 0) .Or. (Low_hour .Gt. 23) .Or.
     $			  (High_hour .Lt. 0) .Or. (High_hour .Gt. 23))
     $			  Call Lib$stop(Stretch_hourserr)

C	    Loop across range to mark

	    Do Ihour = Low_hour, High_hour
	      Str_hours(Ihour) = .True.
	    End Do

C	    Get next spec

	    Got_spec = Cli$get_value('HOURS',hour_spec)
	  End Do

C	  Check final status

	  If (Got_spec .Ne. %Loc(Cli$_absent))
     $	      Call Lib$stop(Stretch_hourserr,,%Val(Got_spec))
	End If
	End
	Subroutine Str$get_quals

C o Load the qualifier values from the command line

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Codes

	External Stretch_beforeerr, Stretch_sinceerr, Stretch_intervalerr
	External Stretch_avgerr, Stretch_pageerr, Stretch_maxerr
	External Stretch_sincerqdavg

C	Functions

	Integer Cli$present, Cli$get_value, Lib$cvt_atime, Lib$cvt_dtime
	Integer Ots$cvt_ti_l, Ots$cvt_t_f

C	Local store

	Integer Istat, Zeroq(2) /0,0/, Istack
	Character*8 Page_spec
	Character*24 Before_spec, Since_spec, Avg_spec
	Character*24 Interval_spec, Max_spec

C	Get the /AVERAGE value (if specified)

	Str_avg_present = Cli$get_value('AVERAGE', Avg_spec)
	If (Str_avg_present) Then
	  If (.Not. Cli$present('SINCE'))
     $		  Call Lib$stop(Stretch_sincerqdavg)
	  Istat = Lib$cvt_dtime(Avg_spec, Str_avg)
	  If (.Not. Istat) Call Lib$stop(Stretch_avgerr,, %Val(Istat))
	  Call Qmath$subtract(Zeroq, Str_avg, Str_avg)
	Endif

C	Get the /LOG qualifier

	Str_log = Cli$present('LOG')

C	Get the /NORMALIZE qualifier

	Str_normalize = Cli$present('NORMALIZE')

C	Get the /SQUEEZE qualifier

	Str_squeeze = Cli$present('SQUEEZE')

C	Get the /BIAS qualifier

	Str_bias = Cli$present('BIAS')

C	Get the /HOURS and /DAYS arrays loaded

	Call Str$get_hours
	Call Str$get_days

C	Get the /INTERVAL

	Istat = Cli$get_value('INTERVAL', Interval_spec)
	If (.Not. Istat) Call Lib$stop(Stretch_intervalerr,, %Val(Istat))
	Istat = Lib$cvt_dtime(Interval_spec, Str_interval)
	If (.Not. Istat) Call Lib$stop(Stretch_intervalerr,, %Val(Istat))
	Call Qmath$subtract(Zeroq, Str_interval, Str_interval)

C	Get the /MAXIMUM value (if specified)

	Str_max_present = Cli$get_value('MAXIMUM', Max_spec)
	If (Str_max_present) Then
	  Istat = Ots$cvt_t_f(Max_spec, Str_max,,, %Val(1))
	  If (.Not. Istat) Call Lib$stop(Stretch_maxerr,, %Val(Istat))
	  If (Str_max .Le. 0.0) Call Lib$stop(Stretch_maxerr)
	Endif

C	Get /PAGE=n

	Istat = Cli$get_value('PAGE', Page_spec)
	If (.Not. Istat) Call Lib$stop(Stretch_pageerr,, %Val(Istat))
	Istat = Ots$cvt_ti_l(Page_spec, Str_pagelength, %Val(4), %Val('9'x))
	If (.Not. Istat) Call Lib$stop(Stretch_pageerr,, %Val(Istat))
	Str_pagelength = Str_pagelength - 4
	If (Str_pagelength .Lt. 8) Str_pagelength = 8

C	Get the /BEFORE qualifier, and convert to quadword form.

	Str_before(1) = -1
	Str_before(2) = -1
	Istat = Cli$get_value('BEFORE', Before_spec)
	If (Istat) Then
	  Istat = Lib$cvt_atime(Before_spec, Str_before)
	  If (.Not. Istat) Call Lib$stop(Stretch_beforeerr,, %Val(Istat))
	Endif

C	Get the /SINCE qualifier, and convert to quadword form.  Set
C	as start of first bucket.

	Str_since(1) = -1
	Str_since(2) = -1
	Istat = Cli$get_value('SINCE', Since_spec)
	If (Istat) Then
	  Istat = Lib$cvt_atime(Since_spec, Str_Since)
	  If (.Not. Istat) Call Lib$stop(Stretch_sinceerr,, %Val(Istat))
	  Str_first_bucket = 0
	  Str_last_bucket = 0
	  Str_bucket_begin(1) = Str_since(1)
	  Str_bucket_begin(2) = Str_since(2)
	Endif

C	Get /STACK qualifier

	Str_stack = Cli$present('STACK')
	Do Istack = 1, Str$k_max_stack
	  Str_stack_title(Istack) = ' '
	  Istat = Cli$get_value('STACK',Str_stack_title(Istack))
	  Call Str$trim(Str_stack_title(Istack),Str_stack_title(Istack),
     $		      Str_stack_title_len(Istack))
	End Do

C	Get the /TITLE value

	Istat = Cli$get_value('TITLE', Str_title)

C	Get the /OUTPUT filespec

	Istat = Cli$get_value('OUTPUT', Str_outfile)
	If (.Not. Istat) Str_outfile = 'NLA0:'
	End
	Integer Function Str$normalize_buckets(Minval, Maxval)

C o This routine adjusts the values in the bucket array.
C   The adjusted MINVAL (minimum) and MAXVAL (maximum) values are returned.
C   The number of buckets in the range is returned (0 if none).

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Parameters

	Real Minval, Maxval

C	Local store

	Integer Ibucket, Nbucket, This_time(2), Delta_time(2), Navg(2)
	Integer Avgbucket, Avgnbucket, Istack
	Logical Found_data

C	Return if no data

	If (Str_first_bucket .Lt. 0) Then
	  Str$normalize_buckets = 0
	  Return
	End If

C	Calculate number of buckets that have data

	Str$normalize_buckets = Str_last_bucket - Str_first_bucket + 1
	If (Str$normalize_buckets .Lt. 1)
     $		  Str$normalize_buckets =
     $		  Str$normalize_buckets + Str$k_max_buckets

C	If doing /AVERAGE, then fold over buckets

	If (Str_avg_present) Then
	  Delta_time(1) = 0
	  Delta_time(2) = 0

C	  Loop across valid bucket range to fold over

	  Do Ibucket = Str_first_bucket,
     $		      Str_first_bucket + Str$normalize_buckets - 1

C	    Get index of next bucket

	    Nbucket = Ibucket
	    If (Nbucket .Ge. Str$k_max_buckets)
     $		      Nbucket = Nbucket - Str$k_max_buckets

C	    Take delta time for this bucket modulo the averaging interval.

	    Call Qmath$divide(Delta_time, Str_avg, Navg)
	    Call Qmath$multiply(Navg, Str_avg, Navg)
	    Call Qmath$subtract(Delta_time, Navg, Navg)

C	    Now find the bucket corresponding to the residue we have

	    Call Qmath$divide(Navg, Str_interval, Navg)
	    Avgbucket = Navg(1) + Str_first_bucket

C	    If not the same bucket, then add over this bucket's contents

	    If (Avgbucket .Ne. Ibucket) Then

C	      Get index of this bucket

	      Avgnbucket = Avgbucket
	      If (Avgnbucket .Ge. Str$k_max_buckets)
     $		      Avgnbucket = Avgnbucket - Str$k_max_buckets

C	      Add over contents

	      Str$normalize_buckets = Str$normalize_buckets - 1
	      Do Istack = 1, Str_stackcnt
	        Str_buckets(Istack,Avgnbucket) = Str_buckets(Istack,Nbucket) +
     $		      Str_buckets(Istack,Avgnbucket)
	        Str_bucket_cnt(Istack,Avgnbucket) =
     $		      Str_bucket_cnt(Istack,Nbucket) +
     $		      Str_bucket_cnt(Istack,Avgnbucket)
	      End Do
	    End If

C	    Bump delta time for next bucket

	    Call Qmath$add(Delta_time, Str_interval, Delta_time)
	  End Do
	End If

C	Initialize min and max for loop

	Minval = 1E30
	Maxval = -1E30

C	Make a pass over the bucket array to average and compute min and max

	Found_data = .False.
	Do Ibucket = Str_first_bucket,
     $		      Str_first_bucket + Str$normalize_buckets - 1

C	  Get index of next bucket

	  Nbucket = Ibucket
	  If (Nbucket .Ge. Str$k_max_buckets)
     $		      Nbucket = Nbucket - Str$k_max_buckets

C	    Loop across all stack values

	    Do Istack = 1, Str_stackcnt

C	      Adjust bucket if there is data

	      If (Str_bucket_cnt(Istack,Nbucket) .Gt. 0) Then

C	      Note that there is valid data

	      Found_data = .True.

C	      Average by sample count

	      Str_buckets(Istack,Nbucket) =
     $		      Str_buckets(Istack,Nbucket) /
     $		      Str_bucket_cnt(Istack,Nbucket)

C	      If biasing, bias out smallest sample value for this stack

	      If (Str_bias)
     $		      Str_buckets(Istack,Nbucket) =
     $		      Str_buckets(Istack,Nbucket) - Str_minval(Istack)

C	      Adjust min and max as required

	      Minval = Min(Str_buckets(Istack,Nbucket), Minval)
	      Maxval = Max(Str_buckets(Istack,Nbucket), Maxval)
	    End If
	  End Do
	End Do

C	Return if no data

	If (.Not. Found_data) Then
	  Str$normalize_buckets = 0
	  Return
	End If

C	Normalize if requested

	If (Str_normalize) Then

C	  Loop across the bucket range to normalize

	  Minval = 1E30
	  Maxval = -1E30
	  Do Ibucket = Str_first_bucket, Str_first_bucket +
     $		      Str$normalize_buckets - 1

C	    Get index of bucket

	    Nbucket = Ibucket
	    If (Nbucket .Ge. Str$k_max_buckets)
     $		      Nbucket = Nbucket - Str$k_max_buckets

C	    Loop across all stack values

	    Do Istack = 1, Str_stackcnt

C	      Normalize bucket if there is data

	      If (Str_buckets(Istack,Nbucket) .Gt. 0)  Then
	        Str_buckets(Istack,Nbucket) =
     $			  Str_buckets(Istack,Nbucket) /
     $			  Float(Str_minval(Istack))
	        Minval = Min(Minval, Str_buckets(Istack,Nbucket))
	        Maxval = Max(Maxval, Str_buckets(Istack,Nbucket))
	      Else
	        Str_buckets(Istack,Nbucket) = 0.0
	      End If	
	    End Do
	  End Do
	End If
	End
	Subroutine Str$print_bar(Bucket, Stackval, Time, Increment)

C o This routine controls the emission of a graph line for the indicated
C   Bucket.  Time is the start time for the bucket.

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Functions

	Logical Str$check_day_hour

C	Parameters

	Integer Time(2), Bucket, Stackval
	Real Increment

C	Local store

	Record /Str_barline_struc/ Barline, Stackhead
	Integer Bar_len, Day, Zeroq(2), This_time(2)
	Character*(4*7) Days /'(Mo)(Tu)(We)(Th)(Fr)(Sa)(Su)'/
	Character*17 Date_spec
	Logical Stackhead_printed

C	Initialize bar

	Barline.bar = Str_divider
	Barline.Overflow = ' '
	Barline.Stack_title = ' '
	Barline.Val = ' '

C	Fill in prefix

	If (Str_avg_present) Then
	  Call Qmath$subtract(Str_bucket_begin, Time, This_time)
	  If ((This_time(1) .Eq. 0) .And. (This_time(2) .Eq. 0)) Then
	    This_time(1) = -1
	    This_time(2) = -1
	  End If
	  Call Sys$asctim(, Barline.date, This_time,)
	  Barline.day = ' '
	Else
	  Call Sys$asctim(, Date_spec, Time,)
	  Barline.date = Date_spec(1:7)//Date_spec(10:17)
	  Call Lib$day_of_week(Time, Day)
	  Barline.day = Days((4 * Day) - 3: )
	End If

C	If stacking, then save the line just built as the stack
C	header.  Then reformat a stack line.

	If (Str_stack) Then
	  If (Stackval .Eq. 1) Then
	    Stackhead_printed = .False.
	    Stackhead = Barline
	    Stackhead.Val = ' '
	    Stackhead.Overflow = ' '
	  End If
	  Barline.Stack_title = ' '
	  Barline.Stack_title(21 - Str_stack_title_len(Stackval):) =
     $		      Str_stack_title(Stackval)
	End If

C	If the bucket is non-empty then display

	If (Str_bucket_cnt(Stackval,Bucket) .Gt. 0) Then

C	  Insert bucket value at end of bar

	  If (Str_buckets(Stackval,Bucket) .Lt. 100.0) Then
	    Write(Barline.Val, Fmt = '(F8.4)') Str_buckets(Stackval,Bucket)
	  Else
	    Write(Barline.Val, Fmt = '(I8)')
     $		      Int(Str_buckets(Stackval,Bucket) + 0.5)
	  End If

C	  Calculate length of bar

	  Bar_len = (Str_buckets(Stackval,Bucket) / Increment) + 1.5

C	  If the length overflows, then insert overflow indicator

	  If (Bar_len .Gt. (Str$k_barlen + 1)) Then
	    Barline.bar = Str_stars
	    Barline.Overflow = '!'

C	  Otherwise, insert stars of the appropriate length

	  Else
	    If (Bar_len .Gt. 0) Barline.bar(1:Bar_len) = Str_stars
	  End If

C	  If stacking and stack header not yet printed, then emit

	  If (Str_stack .And. (.Not. Stackhead_printed)) Then
	    Call Str$print_line(Stackhead.Bar_char)
	    Stackhead_printed = .True.
	  End If

C	  Emit the line

	  Call Str$print_line(Barline.Bar_char)

C	Else, bucket is empty

	Else If (.Not. Str_squeeze) Then
	  If (Str$check_day_hour(Time, Str_interval)) Then
	    Barline.Bar(1:1) = '.'
	    Barline.Val = ' '

C	    If stacking and stack header not yet printed, then emit

	    If (Str_stack .And. (.Not. Stackhead_printed)) Then
	      Call Str$print_line(Stackhead.Bar_char)
	      Stackhead_printed = .True.
	    End If
	    Call Str$print_line(Barline.Bar_char)
	  End If
	End If
	End
	Subroutine Str$print_bottom_page

C o Emit a page footer (if not line 0)

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Local constants

	Character Ff /12/

C	If line zero, then this page is blank.  Just return

	If (Str_linenum .Eq. 0) Return

C	If graphing, put closure

	If (Str_graphing) Then
	  Call Str$emit_line('                     '//Str_dashes)
	  Call Str$emit_line('                     '//Str_header)
	  Call Str$emit_line(' ')
	  Call Str$emit_line('                     '//
     $		      Str_xlabel(1:Str_xlabel_len))
	End If

C	Call to print page feed if beyond end

	If (Str_linenum .Gt. Str_pagelength) Then
	  Call Str$emit_line(Ff)
	  Str_linenum = 0
	End If
	End
	Subroutine Str$print_top_page

C o Called when at top of graph page to print header and bump page count

	Implicit None
	Include 'Stretch.Inc/Nolist'

C	Local Store

	Character*132 Line

C	Set line/page counts

	Str_pagenum = Str_pagenum + 1
	Str_linenum = 0

C	Print some blank lines

	Call Str$emit_line(' ')
	Call Str$emit_line(' ')

C	Print the title and page

	Line = ' '
	Call Sys$fao('!100AS !17%D   Page: !SL',, Line, Str_title,,
     $		      %Val(Str_pagenum))
	Call Str$emit_line(Line)

C	Blank line

	Call Str$emit_line(' ')

C	Print graph header if graphing

	If (Str_graphing) Then
	  Line = ' '
	  Call Sys$fao('          Data Analyzed from: !%D  to  !%D',, Line,
     $		      Str_earliest, Str_latest)
	  Call Str$emit_line(Line)
	  Call Str$emit_line(' ')
	  Call Str$emit_line('                     '//Str_header)
	  Call Str$emit_line('                     '//Str_dashes)
	End If
	End
