class EL_STRING_CHAIN

(source code)

description

Chain of strings conforming to STRING_GENERAL

note
	description: "Chain of strings conforming to ${STRING_GENERAL}"

	author: "Finnian Reilly"
	copyright: "Copyright (c) 2001-2022 Finnian Reilly"
	contact: "finnian at eiffel hyphen loop dot com"

	license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
	date: "2024-10-02 14:19:51 GMT (Wednesday 2nd October 2024)"
	revision: "55"

deferred class
	EL_STRING_CHAIN [S -> STRING_GENERAL create make end]

inherit
	EL_CHAIN [S]
		rename
			joined as joined_chain
		end

	HASHABLE

	EL_LINEAR_STRINGS [S]
		undefine
			find_first_equal, search, has, occurrences, off
		end

	PART_COMPARATOR [S]
		undefine
			copy, is_equal
		end

	EL_MODULE_ITERABLE; EL_MODULE_CONVERT_STRING

	EL_STRING_GENERAL_ROUTINES

feature {NONE} -- Initialization

	make (n: INTEGER)
		deferred
		end

	make_empty
		deferred
		end

	make_from (container: CONTAINER [S])
		do
			if attached as_structure (container) as structure then
				make_from_special (structure.to_special)
			else
				make_empty
			end
		end

	make_from_special (a_area: SPECIAL [S])
		deferred
		end

	make_from_substrings (a_string: READABLE_STRING_GENERAL; a_start_index: INTEGER; count_list: ITERABLE [INTEGER])
		-- make from consecutive substrings in `a_string' with counts in `count_list' starting from `a_start_index'
		do
			make_empty
			append_substrings (a_string, a_start_index, count_list)
		end

	make_comma_split (a_string: READABLE_STRING_GENERAL)
		do
			make_adjusted_split (a_string, ',', {EL_SIDE}.Left)
		end

	make_with_lines (a_string: READABLE_STRING_GENERAL)
		do
			make_split (a_string, '%N')
		end

	make_adjusted_split (a_string: READABLE_STRING_GENERAL; delimiter: CHARACTER_32; adjustments: INTEGER)
		require
			valid_adjustments: valid_adjustments (adjustments)
		do
			make_empty
			append_split (a_string, delimiter, adjustments)
		end

	make_split (a_string: READABLE_STRING_GENERAL; delimiter: CHARACTER_32)
		do
			make_empty
			append_split (a_string, delimiter, 0)
		end

	make_word_split (a_string: READABLE_STRING_GENERAL)
		do
			make_split (a_string, ' ')
		end

feature -- Measurement

	item_count: INTEGER
		do
			Result := item.count
		end

	item_indent: INTEGER
		local
			i: INTEGER; done: BOOLEAN
		do
			from i := 1 until done or i > item.count loop
				if item [i] = '%T' then
					Result := Result + 1
				else
					done := True
				end
				i := i + 1
			end
		end

	longest_count: INTEGER
		-- count of longest string
		do
			push_cursor
			from start until after loop
				Result := Result.max (item.count)
				forth
			end
			pop_cursor
		end

feature -- Access

	first_or_empty: S
		do
			if count > 0 then
				Result := first
			else
				create Result.make (0)
			end
		end

feature -- Status query

	is_indented: BOOLEAN
		-- `True' if all non-empty lines start with a tab character
		 do
		 	Result := across Current as str all
		 		str.item.count > 0 implies str.item.starts_with (Tabulation)
		 	end
		 end

	same_items (a_list: ITERABLE [S]): BOOLEAN
		local
			l_cursor: ITERATION_CURSOR [S]
		do
			push_cursor
			if Iterable.count (a_list) = count then
				Result := True
				from start; l_cursor := a_list.new_cursor until after or not Result loop
					Result := item ~ l_cursor.item
					forth; l_cursor.forth
				end
			end
			pop_cursor
		end

feature -- Format items

	indent (tab_count: INTEGER)
			-- prepend `tab_count' tab character to each line
		require
			valid_tab_count: tab_count >= 0
		local
			l_tab_string: like tab_string
		do
			if tab_count > 0 then
				push_cursor
				l_tab_string := tab_string (tab_count)
				from start until after loop
					item.prepend (l_tab_string)
					forth
				end
				pop_cursor
			end
		ensure
			indented: tab_count > 0 implies is_indented
		end

	indent_item (tab_count: INTEGER)
			-- prepend one tab character to each line
		require
			not_off: not off
			valid_tab_count: tab_count >= 0
		do
			if tab_count > 0 then
				item.prepend (tab_string (tab_count))
			end
		ensure
			tab_count_extra: old item_indent + tab_count = item_indent
		end

	left_adjust
		-- left adjust all lines
		do
			push_cursor
			from start until after loop
				item.left_adjust
				forth
			end
			pop_cursor
		end

	right_pad (column_widths: ARRAY [INTEGER])
		do
			push_cursor
			from start until after or else index > column_widths.upper loop
				from until item_count >= column_widths [index] loop
					item.append_code (32)
				end
				forth
			end
			pop_cursor
		end

	right_adjust
		do
			push_cursor
			from start until after loop
				item.right_adjust
				forth
			end
			pop_cursor
		end

	unindent (tab_count: INTEGER)
		-- remove maximum of `tab_count' tab characters from the start of each line `item'
		require
			is_indented: is_indented
		local
			smaller_count: INTEGER
		do
			push_cursor
			from start until after loop
				smaller_count := tab_count.min (item_indent)
				if smaller_count > 0 then
					item.keep_tail (item.count - smaller_count)
				end
				forth
			end
			pop_cursor
		end

feature -- Element change

	append_comma_separated (a_string: READABLE_STRING_GENERAL)
		do
			append_split (a_string, ',', {EL_SIDE}.Left)
		end

	append_split (a_string: READABLE_STRING_GENERAL; delimiter: CHARACTER_32; adjustments: INTEGER)
		require
			valid_adjustments: valid_adjustments (adjustments)
		do
			if attached Split_intervals as intervals then
				intervals.wipe_out
				intervals.fill (a_string, delimiter, adjustments)
				append_intervals (a_string, intervals)
			end
		end

	append_substrings (a_string: READABLE_STRING_GENERAL; a_start_index: INTEGER; count_list: ITERABLE [INTEGER])
		-- append consecutive substrings in `a_string' with counts in `count_list' starting from `a_start_index'
		local
			start_index, end_index: INTEGER
		do
			if a_string.valid_index (a_start_index) and then attached Split_intervals as intervals then
				intervals.wipe_out
				start_index := a_start_index
				across count_list as n loop
					end_index := start_index + n.item - 1
					if a_string.valid_index (end_index) then
						intervals.extend (start_index, end_index)
					end
					start_index := end_index + 1
				end
				append_intervals (a_string, intervals)
			end
		end

	append_tuple (tuple: TUPLE)
		local
			i: INTEGER; string: S
		do
			grow (count + tuple.count)
			from i := 1 until i > tuple.count loop
				if tuple.is_reference_item (i) and then attached tuple.reference_item (i) as any_ref then
					if attached {READABLE_STRING_GENERAL} any_ref as general then
						string := new_string (general)
					elseif attached {EL_PATH} any_ref as path then
						string := new_string (path.to_string)
					end
				else
					string := new_string (tuple.item (i).out)
				end
				extend (string)
				i := i + 1
			end
		end

	append_general (list: ITERABLE [READABLE_STRING_GENERAL])
		do
			grow (count + Iterable.count (list))
			across list as general loop
				if attached {S} general.item as str then
					extend (str)
				else
					extend (new_string (general.item))
				end
			end
		end

	set_first_and_last (a_first, a_last: S)
		do
			if not is_empty then
				put_i_th (a_first, 1); put_i_th (a_last, count)
			end
		end

feature -- Basic operations

	sort (in_ascending_order: BOOLEAN)
		local
			quick: QUICK_SORTER [S]
		do
			create quick.make (Current)

			if in_ascending_order then
				quick.sort (Current)
			else
				quick.reverse_sort (Current)
			end
		end

feature -- Removal

	prune_all_empty
			-- Remove empty items
		do
			from start until after loop
				if item.is_empty then
					remove
				else
					forth
				end
			end
		end

	unique_sort
		-- ascending sort removing duplicates
		local
			previous: like item
		do
			sort (True)
			from start until after loop
				if index = 1 then
					previous := item
					forth

				elseif item ~ previous then
					remove
				else
					previous := item
					forth
				end
			end
		end

	wrap (line_width: INTEGER)
		local
			previous_item: S
		do
			push_cursor
			if not is_empty then
				from start; forth until after loop
					previous_item := i_th (index - 1)
					if (previous_item.count + item.count + 1) < line_width then
						previous_item.append_code (32)
						previous_item.append (item)
						remove
					else
						forth
					end
				end
			end
			pop_cursor
		end

feature -- Resizing

	grow (i: INTEGER)
			-- Change the capacity to at least `i'.
		deferred
		end

feature -- Contract Support

	valid_adjustments (bitmap: INTEGER): BOOLEAN
		do
			Result := 0 <= bitmap and then bitmap <= {EL_SIDE}.Both
		end

feature {NONE} -- Implementation

	append_intervals (a_string: READABLE_STRING_GENERAL; intervals: EL_SPLIT_INTERVALS)
		do
			grow (count + intervals.count)
			from intervals.start until intervals.after loop
				extend (new_string (a_string.substring (intervals.item_lower, intervals.item_upper)))
				intervals.forth
			end
		end

	less_than (u, v: S): BOOLEAN
		do
			Result := u.is_less (v)
		end

	new_string (general: READABLE_STRING_GENERAL): S
		do
			if attached {S} general as str then
				Result := str
			else
				create Result.make (general.count)
				if is_zstring (general) then
					as_zstring (general).append_to_general (Result)
				else
					Result.append (general)
				end
			end
		end

	tab_string (n: INTEGER): S
		local
			i: INTEGER
		do
			create Result.make (n)
			from i := 1 until i > n loop
				Result.append_code ({EL_ASCII}.Tab)
				i := i + 1
			end
		end

end