File: DataLoadSave\Text\TextSaver.cs
Web Access
Project: src\src\Microsoft.ML.Data\Microsoft.ML.Data.csproj (Microsoft.ML.Data)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.ML;
using Microsoft.ML.CommandLine;
using Microsoft.ML.Data;
using Microsoft.ML.Data.Conversion;
using Microsoft.ML.Data.IO;
using Microsoft.ML.Internal.Utilities;
using Microsoft.ML.Runtime;
 
[assembly: LoadableClass(TextSaver.Summary, typeof(TextSaver), typeof(TextSaver.Arguments), typeof(SignatureDataSaver),
    "Text Saver", "TextSaver", "Text", DocName = "saver/TextSaver.md")]
 
namespace Microsoft.ML.Data.IO
{
    [BestFriend]
    internal sealed class TextSaver : IDataSaver
    {
        internal static class Defaults
        {
            internal const char Separator = '\t';
            internal const bool ForceDense = false;
            internal const bool OutputSchema = true;
            internal const bool OutputHeader = true;
        }
 
        // REVIEW: consider saving a command line in a separate file.
        public sealed class Arguments
        {
            [Argument(ArgumentType.AtMostOnce, HelpText = "Separator", ShortName = "sep")]
            public string Separator = Defaults.Separator.ToString();
 
            [Argument(ArgumentType.AtMostOnce, HelpText = "Force dense format", ShortName = "dense")]
            public bool Dense = Defaults.ForceDense;
 
            // REVIEW: This and the corresponding BinarySaver option should be removed,
            // with the silence being handled, somehow, at the environment level. (Task 6158846.)
            [Argument(ArgumentType.LastOccurrenceWins, HelpText = "Suppress any info output (not warnings or errors)", Hide = true)]
            public bool Silent;
 
            [Argument(ArgumentType.AtMostOnce, HelpText = "Output the comment containing the loader settings", ShortName = "schema")]
            public bool OutputSchema = Defaults.OutputSchema;
 
            [Argument(ArgumentType.AtMostOnce, HelpText = "Output the header", ShortName = "header")]
            public bool OutputHeader = Defaults.OutputHeader;
        }
 
        internal const string Summary = "Writes data into a text file.";
 
        private abstract class ValueWriter
        {
            public readonly int Source;
 
            public static ValueWriter Create(DataViewRowCursor cursor, int col, char sep)
            {
                Contracts.AssertValue(cursor);
 
                DataViewType type = cursor.Schema[col].Type;
                Type writePipeType;
                if (type is VectorDataViewType vectorType)
                    writePipeType = typeof(VecValueWriter<>).MakeGenericType(vectorType.ItemType.RawType);
                else
                    writePipeType = typeof(ValueWriter<>).MakeGenericType(type.RawType);
 
                return (ValueWriter)Activator.CreateInstance(writePipeType, cursor, type, col, sep);
            }
 
            public abstract string Default { get; }
 
            public ValueWriter(int source)
            {
                Source = source;
            }
 
            /// <summary>
            /// Write the data to the given stream. This requires that FetchData was previously called.
            /// </summary>
            public abstract void WriteData(Action<StringBuilder, int> appendItem, out int length);
 
            public abstract void WriteHeader(Action<StringBuilder, int> appendItem, out int length);
        }
 
        private abstract class ValueWriterBase<T> : ValueWriter
        {
            protected readonly ValueMapper<T, StringBuilder> Conv;
            protected readonly char Sep;
            protected StringBuilder Sb;
 
            public override string Default { get; }
 
            protected ValueWriterBase(PrimitiveDataViewType type, int source, char sep)
                : base(source)
            {
                Contracts.Assert(type.IsStandardScalar() || type is KeyDataViewType);
                Contracts.Assert(type.RawType == typeof(T));
 
                Sep = sep;
                if (type is TextDataViewType)
                {
                    // For text we need to deal with escaping.
                    ValueMapper<ReadOnlyMemory<char>, StringBuilder> c = MapText;
                    Conv = (ValueMapper<T, StringBuilder>)(Delegate)c;
                }
                else if (type is TimeSpanDataViewType)
                {
                    ValueMapper<TimeSpan, StringBuilder> c = MapTimeSpan;
                    Conv = (ValueMapper<T, StringBuilder>)(Delegate)c;
                }
                else if (type is DateTimeDataViewType)
                {
                    ValueMapper<DateTime, StringBuilder> c = MapDateTime;
                    Conv = (ValueMapper<T, StringBuilder>)(Delegate)c;
                }
                else if (type is DateTimeOffsetDataViewType)
                {
                    ValueMapper<DateTimeOffset, StringBuilder> c = MapDateTimeZone;
                    Conv = (ValueMapper<T, StringBuilder>)(Delegate)c;
                }
                else
                    Conv = Conversions.DefaultInstance.GetStringConversion<T>(type);
 
                var d = default(T);
                Conv(in d, ref Sb);
                Default = Sb.ToString();
            }
 
            protected void MapText(in ReadOnlyMemory<char> src, ref StringBuilder sb)
            {
                TextSaverUtils.MapText(src.Span, ref sb, Sep);
            }
 
            protected void MapTimeSpan(in TimeSpan src, ref StringBuilder sb)
            {
                TextSaverUtils.MapTimeSpan(in src, ref sb);
            }
 
            protected void MapDateTime(in DateTime src, ref StringBuilder sb)
            {
                TextSaverUtils.MapDateTime(in src, ref sb);
            }
 
            protected void MapDateTimeZone(in DateTimeOffset src, ref StringBuilder sb)
            {
                TextSaverUtils.MapDateTimeZone(in src, ref sb);
            }
        }
 
        private sealed class VecValueWriter<T> : ValueWriterBase<T>
        {
            private readonly ValueGetter<VBuffer<T>> _getSrc;
            private VBuffer<T> _src;
            private readonly VBuffer<ReadOnlyMemory<char>> _slotNames;
            private readonly int _slotCount;
 
            public VecValueWriter(DataViewRowCursor cursor, VectorDataViewType type, int source, char sep)
                : base(type.ItemType, source, sep)
            {
                _getSrc = cursor.GetGetter<VBuffer<T>>(cursor.Schema[source]);
                VectorDataViewType typeNames;
                if (type.IsKnownSize
                    && (typeNames = cursor.Schema[source].Annotations.Schema.GetColumnOrNull(AnnotationUtils.Kinds.SlotNames)?.Type as VectorDataViewType) != null
                    && typeNames.Size == type.Size && typeNames.ItemType is TextDataViewType)
                {
                    cursor.Schema[source].Annotations.GetValue(AnnotationUtils.Kinds.SlotNames, ref _slotNames);
                    Contracts.Check(_slotNames.Length == typeNames.Size, "Unexpected slot names length");
                }
                _slotCount = type.Size;
            }
 
            public override void WriteData(Action<StringBuilder, int> appendItem, out int length)
            {
                _getSrc(ref _src);
                var srcValues = _src.GetValues();
                if (_src.IsDense)
                {
                    for (int i = 0; i < srcValues.Length; i++)
                    {
                        Conv(in srcValues[i], ref Sb);
                        appendItem(Sb, i);
                    }
                }
                else
                {
                    var srcIndices = _src.GetIndices();
                    for (int i = 0; i < srcValues.Length; i++)
                    {
                        Conv(in srcValues[i], ref Sb);
                        appendItem(Sb, srcIndices[i]);
                    }
                }
                length = _src.Length;
            }
 
            public override void WriteHeader(Action<StringBuilder, int> appendItem, out int length)
            {
                length = _slotCount;
                var slotNamesValues = _slotNames.GetValues();
                if (slotNamesValues.Length == 0)
                    return;
 
                var slotNamesIndices = _slotNames.GetIndices();
                for (int i = 0; i < slotNamesValues.Length; i++)
                {
                    var name = slotNamesValues[i];
                    if (name.IsEmpty)
                        continue;
                    MapText(in name, ref Sb);
                    int index = _slotNames.IsDense ? i : slotNamesIndices[i];
                    appendItem(Sb, index);
                }
            }
        }
 
        private sealed class ValueWriter<T> : ValueWriterBase<T>
        {
            private readonly ValueGetter<T> _getSrc;
            private T _src;
            private readonly string _columnName;
 
            public ValueWriter(DataViewRowCursor cursor, PrimitiveDataViewType type, int source, char sep)
                : base(type, source, sep)
            {
                var column = cursor.Schema[source];
                _getSrc = cursor.GetGetter<T>(column);
                _columnName = column.Name;
            }
 
            public override void WriteData(Action<StringBuilder, int> appendItem, out int length)
            {
                _getSrc(ref _src);
                Conv(in _src, ref Sb);
                appendItem(Sb, 0);
                length = 1;
            }
 
            public override void WriteHeader(Action<StringBuilder, int> appendItem, out int length)
            {
                var span = _columnName.AsMemory();
                MapText(in span, ref Sb);
                appendItem(Sb, 0);
                length = 1;
            }
        }
 
        private readonly bool _forceDense;
        private readonly bool _outputSchema;
        private readonly bool _outputHeader;
        private readonly char _sepChar;
        private readonly string _sepStr;
        private readonly bool _silent;
 
        private readonly IHost _host;
 
        // Used to calculate the efficiency of sparse format.
        private const Double _sparseWeight = 2.5;
 
        public TextSaver(IHostEnvironment env, Arguments args)
        {
            Contracts.AssertValue(args);
            Contracts.CheckValue(env, nameof(env));
            _host = env.Register("TextSaver");
            _forceDense = args.Dense;
            _outputSchema = args.OutputSchema;
            _outputHeader = args.OutputHeader;
 
            _sepChar = SepStrToChar(args.Separator);
            _sepStr = _sepChar.ToString();
            _silent = args.Silent;
        }
 
        private static char SepStrToChar(string sep)
        {
            Contracts.CheckUserArg(!string.IsNullOrEmpty(sep), nameof(Arguments.Separator), "Must specify a separator");
            sep = sep.ToLowerInvariant();
            switch (sep)
            {
                case "space":
                case " ":
                    return ' ';
                case "tab":
                case "\t":
                    return '\t';
                case "comma":
                case ",":
                    return ',';
                case "semicolon":
                case ";":
                    return ';';
                case "bar":
                case "|":
                    return '|';
                default:
                    throw Contracts.ExceptUserArg(nameof(Arguments.Separator), "Invalid separator - must be: space, tab, comma, semicolon, or bar");
            }
        }
 
        /// <summary>
        /// Returns the string representation of a separator: helpful if it's whitespace or a punctuation mark.
        /// </summary>
        public static string SeparatorCharToString(char separator)
        {
            switch (separator)
            {
                case ' ':
                    return "space";
                case '\t':
                    return "tab";
                case ',':
                    return "comma";
                case ';':
                    return "semicolon";
                case '|':
                    return "bar";
 
                default:
                    return separator.ToString();
            }
        }
 
        public bool IsColumnSavable(DataViewType type)
        {
            var item = type.GetItemType();
            return item.IsStandardScalar() || item is KeyDataViewType;
        }
 
        public void SaveData(Stream stream, IDataView data, params int[] cols)
        {
            string argsLoader;
            SaveData(out argsLoader, stream, data, cols);
        }
 
        public void SaveData(out string argsLoader, Stream stream, IDataView data, params int[] cols)
        {
            _host.CheckValue(stream, nameof(stream));
            _host.CheckValue(data, nameof(data));
            _host.CheckNonEmpty(cols, nameof(cols));
 
            using (var ch = _host.Start("Saving"))
            {
                long count;
                int min;
                int max;
                using (var writer = Utils.OpenWriter(stream))
                    WriteDataCore(ch, writer, data, out argsLoader, out count, out min, out max, cols);
 
                if (!_silent)
                    ShowCount(ch, count, min, max);
            }
        }
 
        public void WriteData(IDataView data, bool showCount, params int[] cols)
        {
            _host.CheckValue(data, nameof(data));
            _host.CheckNonEmpty(cols, nameof(cols));
 
            string argsLoader;
            using (var ch = _host.Start("Writing"))
            {
                long count;
                int min;
                int max;
                using (var writer = new StringWriter())
                {
                    WriteDataCore(ch, writer, data, out argsLoader, out count, out min, out max, cols);
                    ch.Info(MessageSensitivity.UserData | MessageSensitivity.Schema, writer.ToString());
                }
 
                if (showCount)
                    ShowCount(ch, count, min, max);
            }
        }
 
        private void ShowCount(IChannel ch, long count, int min, int max)
        {
            if (count == 0)
                ch.Warning(MessageSensitivity.None, "Wrote zero rows of data!");
            else if (min == max)
                ch.Info(MessageSensitivity.None, "Wrote {0} rows of length {1}", count, min);
            else
                ch.Info(MessageSensitivity.None, "Wrote {0} rows of lengths between {1} and {2}", count, min, max);
        }
 
        private void WriteDataCore(IChannel ch, TextWriter writer, IDataView data,
            out string argsLoader, out long count, out int min, out int max, params int[] cols)
        {
            _host.AssertValue(ch);
            ch.AssertValue(writer);
            ch.AssertValue(data);
            ch.AssertNonEmpty(cols);
 
            // Determine the active columns and whether there is header information.
            var activeCols = new List<DataViewSchema.Column>();
            for (int i = 0; i < cols.Length; i++)
            {
                ch.Check(0 <= cols[i] && cols[i] < data.Schema.Count);
                DataViewType itemType = data.Schema[cols[i]].Type.GetItemType();
                ch.Check(itemType is KeyDataViewType || itemType.IsStandardScalar());
                activeCols.Add(data.Schema[cols[i]]);
            }
 
            bool hasHeader = false;
            if (_outputHeader)
            {
                for (int i = 0; i < cols.Length; i++)
                {
                    if (hasHeader)
                        continue;
                    var type = data.Schema[cols[i]].Type;
                    if (!(type is VectorDataViewType vectorType))
                    {
                        hasHeader = true;
                        continue;
                    }
                    if (!vectorType.IsKnownSize)
                        continue;
                    var typeNames = data.Schema[cols[i]].Annotations.Schema.GetColumnOrNull(AnnotationUtils.Kinds.SlotNames)?.Type as VectorDataViewType;
                    if (typeNames != null && typeNames.Size == vectorType.Size && typeNames.ItemType is TextDataViewType)
                        hasHeader = true;
                }
            }
 
            using (var cursor = data.GetRowCursor(activeCols))
            {
                var pipes = new ValueWriter[cols.Length];
                for (int i = 0; i < cols.Length; i++)
                    pipes[i] = ValueWriter.Create(cursor, cols[i], _sepChar);
 
                // REVIEW: This should be outside the cursor creation.
                string header = CreateLoaderArguments(data.Schema, pipes, hasHeader, ch);
                argsLoader = header;
                if (_outputSchema)
                    WriteSchemaAsComment(writer, header);
 
                double rowCount = data.GetRowCount() ?? double.NaN;
                using (var pch = !_silent ? _host.StartProgressChannel("TextSaver: saving data") : null)
                {
                    long stateCount = 0;
                    var state = new State(this, writer, pipes, hasHeader);
                    if (pch != null)
                        pch.SetHeader(new ProgressHeader(new[] { "rows" }), e => e.SetProgress(0, stateCount, rowCount));
                    state.Run(cursor, ref stateCount, out min, out max);
                    count = stateCount;
                    if (pch != null)
                        pch.Checkpoint(stateCount);
 
                }
            }
        }
 
        private void WriteSchemaAsComment(TextWriter writer, string str)
        {
            writer.WriteLine("#@ TextLoader{");
            foreach (string line in CmdIndenter.GetIndentedCommandLine(str).Split(new[] { "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries))
                writer.WriteLine("#@   " + line);
            writer.WriteLine("#@ }");
        }
 
        private string CreateLoaderArguments(DataViewSchema schema, ValueWriter[] pipes, bool hasHeader, IChannel ch)
        {
            StringBuilder sb = new StringBuilder();
            if (hasHeader)
                sb.Append("header+ ");
            sb.AppendFormat("sep={0}", SeparatorCharToString(_sepChar));
 
            // This variable indicates the start index of each column.
            // If null, it means the index cannot be determined.
            int? index = 0;
            for (int i = 0; i < pipes.Length; i++)
            {
                int src = pipes[i].Source;
                string name = schema[src].Name;
                var type = schema[src].Type;
 
                var column = GetColumn(name, type, index);
                sb.Append(" col=");
                if (!column.TryUnparse(sb))
                {
                    var settings = CmdParser.GetSettings(_host, column, new TextLoader.Column());
                    CmdQuoter.QuoteValue(settings, sb, true);
                }
                if (type is VectorDataViewType vectorType && !vectorType.IsKnownSize && i != pipes.Length - 1)
                {
                    ch.Warning("Column '{0}' is variable length, so it must be the last, or the file will be unreadable. Consider switching to binary format or use xf=Choose to make '{0}' the last column.", name);
                    index = null;
                }
 
                index += type.GetValueCount();
            }
 
            return sb.ToString();
        }
 
        private TextLoader.Column GetColumn(string name, DataViewType type, int? start)
        {
            KeyCount keyCount = null;
            VectorDataViewType vectorType = type as VectorDataViewType;
            DataViewType itemType = vectorType?.ItemType ?? type;
            if (itemType is KeyDataViewType key)
                keyCount = new KeyCount(key.Count);
 
            InternalDataKind kind = itemType.GetRawKind();
 
            TextLoader.Range[] source = null;
            TextLoader.Range range = null;
            int minValue = start ?? -1;
            if (vectorType?.IsKnownSize == true)
                range = new TextLoader.Range { Min = minValue, Max = minValue + vectorType.Size - 1, ForceVector = true };
            else if (vectorType != null)
                range = new TextLoader.Range { Min = minValue, VariableEnd = true };
            else
                range = new TextLoader.Range { Min = minValue };
            source = new TextLoader.Range[1] { range };
            return new TextLoader.Column() { Name = name, KeyCount = keyCount, Source = source, Type = kind };
        }
 
        private sealed class State
        {
            private readonly bool _dense;
            private readonly char _sepChar;
            private readonly string _sepStr;
            private readonly TextWriter _writer;
            private readonly ValueWriter[] _pipes;
            private readonly bool _hasHeader;
            private readonly IHost _host;
 
            // Current column and it's starting destination index.
            private int _col;
            private int _dstBase;
            private int _dstPrev;
 
            // Map from column to starting destination index and slot.
            private readonly int[] _mpcoldst;
            private readonly int[] _mpcolslot;
 
            // "slot" is an index into _mpslotdst and _mpslotichLim. _mpslotdst is the sequence of
            // destination indices. _mpslotichLim is the sequence of upper bounds on the characters
            // for the slots.
            private int _slotLim;
            private int[] _mpslotdst;
            private int[] _mpslotichLim;
 
            // The character buffer.
            private int _cch;
            private char[] _rgch;
 
            public State(TextSaver parent, TextWriter writer, ValueWriter[] pipes, bool hasHeader)
            {
                Contracts.AssertValue(parent);
                Contracts.AssertValue(parent._host);
                _host = parent._host;
                _host.AssertValue(writer);
                _host.AssertValue(pipes);
 
                _dense = parent._forceDense;
                _sepChar = parent._sepChar;
                _sepStr = parent._sepStr;
 
                _writer = writer;
                _pipes = pipes;
                _hasHeader = hasHeader && parent._outputHeader;
 
                _mpcoldst = new int[_pipes.Length + 1];
                _mpcolslot = new int[_pipes.Length + 1];
 
                _rgch = new char[1024];
                _mpslotdst = new int[128];
                _mpslotichLim = new int[128];
            }
 
            public void Run(DataViewRowCursor cursor, ref long count, out int minLen, out int maxLen)
            {
                minLen = int.MaxValue;
                maxLen = 0;
 
                Action<StringBuilder, int> append = (sb, index) => AppendItem(sb, index, _pipes[_col].Default);
                Action<StringBuilder, int> appendHeader = (sb, index) => AppendItem(sb, index, "");
 
                if (_hasHeader)
                {
                    StartLine();
                    while (_col < _pipes.Length)
                    {
                        int len;
                        _pipes[_col].WriteHeader(appendHeader, out len);
                        Contracts.Assert(len >= 0);
                        EndColumn(len);
                    }
                    EndLine("\"\"");
                    _writer.WriteLine();
                }
 
                while (cursor.MoveNext())
                {
                    // Start a new line. This also starts the first column.
                    StartLine();
 
                    while (_col < _pipes.Length)
                    {
                        int len;
                        _pipes[_col].WriteData(append, out len);
                        Contracts.Assert(len >= 0);
                        EndColumn(len);
                    }
 
                    if (minLen > _dstBase)
                        minLen = _dstBase;
                    if (maxLen < _dstBase)
                        maxLen = _dstBase;
 
                    EndLine();
                    _writer.WriteLine();
                    count++;
                }
            }
 
            private void StartLine()
            {
                _cch = 0;
                _slotLim = 0;
                _col = 0;
                _dstBase = 0;
                _dstPrev = -1;
                Contracts.Assert(_mpcoldst[_col] == 0);
                Contracts.Assert(_mpcolslot[_col] == 0);
            }
 
            private bool Matches(StringBuilder sb, string def)
            {
                if (sb.Length != def.Length)
                    return false;
                for (int ich = 0; ich < def.Length; ich++)
                {
                    if (sb[ich] != def[ich])
                        return false;
                }
                return true;
            }
 
            private void AppendItem(StringBuilder sb, int index, string def)
            {
                Contracts.Assert(0 <= _col && _col < _pipes.Length);
                Contracts.AssertValue(sb);
 
                Contracts.Check(index >= 0);
                int dst = checked(_dstBase + index);
                Contracts.Check(dst > _dstPrev);
                _dstPrev = dst;
 
                // Suppress default values.
                if (Matches(sb, def))
                    return;
 
                // Make sure the buffers are big enough.
                int cch = checked(_cch + sb.Length);
                while (cch > _rgch.Length)
                    Array.Resize(ref _rgch, checked(_rgch.Length * 2));
                if (_mpslotdst.Length <= _slotLim)
                    Array.Resize(ref _mpslotdst, checked(_mpslotdst.Length * 2));
                if (_mpslotichLim.Length <= _slotLim)
                    Array.Resize(ref _mpslotichLim, checked(_mpslotichLim.Length * 2));
 
                // Copy the characters and update the mappings.
                sb.CopyTo(0, _rgch, _cch, sb.Length);
                _cch = cch;
                _mpslotichLim[_slotLim] = _cch;
                _mpslotdst[_slotLim] = dst;
                _slotLim++;
            }
 
            private void EndColumn(int length)
            {
                Contracts.Assert(_col < _pipes.Length);
                int dst = checked(_dstBase + length);
                Contracts.Check(_dstPrev < dst);
                ++_col;
                _mpcoldst[_col] = dst;
                _mpcolslot[_col] = _slotLim;
                _dstBase = dst;
            }
 
            private void EndLine(string defaultStr = null)
            {
                Contracts.Assert(_col == _pipes.Length);
 
                if (_dense)
                {
                    WriteDenseTo(_dstBase, defaultStr);
                    return;
                }
 
                // Find the sparse split point.
                // REVIEW: Should we allow splitting at any slot or only at column boundaries?
                // This currently does the latter.
                int colBest = 0;
                Double bestScore = _sparseWeight * _slotLim;
                for (int col = 1; col <= _pipes.Length; col++)
                {
                    int cd = _mpcoldst[col];
                    int cs = _slotLim - _mpcolslot[col];
 
                    Double score = cd + _sparseWeight * cs;
                    if (bestScore > score)
                    {
                        bestScore = score;
                        colBest = col;
                    }
                }
 
                // If the length of the sparse section is small compared to the dense count,
                // don't bother with sparse.
                int lenDense = _mpcoldst[colBest];
                int lenSparse = _dstBase - lenDense;
                if (lenSparse < 5 || lenSparse < lenDense / 5)
                    colBest = _pipes.Length;
 
                string sep = "";
                if (colBest > 0)
                {
                    WriteDenseTo(_mpcoldst[colBest], defaultStr);
                    sep = _sepStr;
                }
 
                if (colBest >= _pipes.Length)
                    return;
 
                // Write the rest sparsely.
                _writer.Write(sep);
                sep = _sepStr;
                _writer.Write(lenSparse);
 
                int slot = _mpcolslot[colBest];
                if (slot == _slotLim)
                {
                    // Need to write at least one sparse specification.
                    _writer.Write(sep);
                    _writer.Write("0:");
                    _writer.Write(defaultStr ?? _pipes[colBest].Default);
                    return;
                }
 
                int ichMin = slot > 0 ? _mpslotichLim[slot - 1] : 0;
                for (; slot < _slotLim; slot++)
                {
                    _writer.Write(sep);
                    _writer.Write(_mpslotdst[slot] - lenDense);
                    _writer.Write(':');
                    int ichLim = _mpslotichLim[slot];
                    _writer.Write(_rgch, ichMin, ichLim - ichMin);
                    ichMin = ichLim;
                }
            }
 
            private void WriteDenseTo(int dstLim, string defaultStr = null)
            {
                Contracts.Assert(_col == _pipes.Length);
                Contracts.Assert(dstLim <= _dstBase);
 
                string sep = "";
                int col = 0;
                string def = defaultStr ?? _pipes[0].Default;
                int slot = 0;
                int ichMin = 0;
                for (int dst = 0; dst < dstLim; dst++)
                {
                    Contracts.Assert(slot == _slotLim || slot < _slotLim && dst <= _mpslotdst[slot]);
 
                    _writer.Write(sep);
                    sep = _sepStr;
                    while (dst >= _mpcoldst[col + 1])
                    {
                        // Move to the next column
                        col++;
                        Contracts.Assert(col < _pipes.Length);
                        def = defaultStr ?? _pipes[col].Default;
                    }
 
                    if (slot == _slotLim || dst < _mpslotdst[slot])
                        _writer.Write(def);
                    else
                    {
                        int ichLim = _mpslotichLim[slot];
                        _writer.Write(_rgch, ichMin, ichLim - ichMin);
                        ichMin = ichLim;
                        slot++;
                    }
                }
            }
        }
    }
 
    internal static class TextSaverUtils
    {
        /// <summary>
        /// Converts a ReadOnlySpan to a StringBuilder using TextSaver escaping and string quoting rules.
        /// </summary>
        internal static void MapText(ReadOnlySpan<char> span, ref StringBuilder sb, char sep)
        {
            if (sb == null)
                sb = new StringBuilder();
            else
                sb.Clear();
 
            if (span.IsEmpty)
                sb.Append("\"\"");
            else
            {
                int ichMin = 0;
                int ichLim = span.Length;
                int ichCur = ichMin;
                int ichRun = ichCur;
                bool quoted = false;
 
                // Strings that start with space need to be quoted.
                Contracts.Assert(ichCur < ichLim);
                if (span[ichCur] == ' ')
                {
                    quoted = true;
                    sb.Append('"');
                }
 
                for (; ichCur < ichLim; ichCur++)
                {
                    char ch = span[ichCur];
                    if (ch != '"' && ch != sep && ch != ':' && ch != '\n')
                        continue;
                    if (!quoted)
                    {
                        Contracts.Assert(ichRun == ichMin);
                        sb.Append('"');
                        quoted = true;
                    }
                    if (ch == '"')
                    {
                        if (ichRun < ichCur)
                            sb.AppendSpan(span.Slice(ichRun, ichCur - ichRun));
                        sb.Append("\"\"");
                        ichRun = ichCur + 1;
                    }
                }
                Contracts.Assert(ichCur == ichLim);
                if (ichRun < ichCur)
                    sb.AppendSpan(span.Slice(ichRun, ichCur - ichRun));
                if (quoted)
                    sb.Append('"');
            }
        }
 
        internal static void MapTimeSpan(in TimeSpan src, ref StringBuilder sb)
        {
            if (sb == null)
                sb = new StringBuilder();
            else
                sb.Clear();
 
            sb.AppendFormat("\"{0:c}\"", src);
        }
 
        internal static void MapDateTime(in DateTime src, ref StringBuilder sb)
        {
            if (sb == null)
                sb = new StringBuilder();
            else
                sb.Clear();
 
            sb.AppendFormat("\"{0:o}\"", src);
        }
 
        internal static void MapDateTimeZone(in DateTimeOffset src, ref StringBuilder sb)
        {
            if (sb == null)
                sb = new StringBuilder();
            else
                sb.Clear();
 
            sb.AppendFormat("\"{0:o}\"", src);
        }
    }
}