Lua String Writer · 2010-02-04 16:35 by Black in Programming Scripts
Lua strings are opaque byte streams. They are constant, and can only be manipulated by using the string api to create new strings. This can be expensive, especially when creating a string by appending new values at the end. While Lua contains optimizations for direct concatenation, successive appending has a high overhead.
This StringWriter class reduces the overhead by aggregating string concatenations in a table and executing them when requested. It was originally designed to serve as an efficient drop-in replacement for files as created by io.open, but it can also be used standalone.
The class itself is built with a protected shared metatable and state inside a table. The state itself is not protected (it would be possible by using individual metatables or an internal database in a weak table, but this is more elegant). The metatable contains entries to redirect reads to the method table, redirect new writes to nothing and prevent changing or reading the metatable. The concatenation operator is also overloaded, but since it has value semantic, and is not allowed to change the object itself, the implementation is less efficient than StringWriter:write(). Converting a StringWriter with tostring() gives the contained string, equivalent to StringWriter:get().
stringwriter.lua [4.77 kB]
- -- MetaTable for string writers
- local StringWriter_Meta = {
- ["__index"] = StringWriter_Methods;
- ["__newindex"] = function ()
- -- Don't allow setting values
- end;
- ["__metatable"] = StringWriter_ID;
- ["__tostring"] = StringWriter_Methods.get;
- ["__concat"] = function (this, str)
- str = tostring(str);
- local sw = StringWriter();
- sw.string_ = {};
- for _, v in ipairs(this.string_) do
- table.insert(sw.string_, v);
- end
- table.insert(sw.string_, str);
- sw.len_ = this.len_ + #str;
- sw.pos_ = sw.len_;
- return sw;
- end;
- }
The method table itself contains all methods the StringWriter supports. It was modeled after the file class, so many methods are placeholders that do nothing. The methods that are supported are seeking and writing. Seeking simply sets an internal position value. Writing in the context of files means overwriting and extending. When the position is at the end, the contents that are to be written can simply be appended to the contents table. Otherwise, the string has to be baked, split, and recomposed.
stringwriter.lua [4.77 kB]
- -- Methods for string writers
- local StringWriter_Methods = {
- ["close"] = voidFunc;
- ["flush"] = voidFunc;
- ["lines"] = voidFunc;
- ["read"] = voidFunc;
- ["seek"] = function (this, base, offset)
- -- Only act on StringWriters
- if not StringWriter_Check(this) then
- return nil, "Invalid StringWriter";
- end;
- -- Default offset
- if type(base) == "number" then
- offset = base; -- Not done in file, but reasonable
- else
- offset = offset or 0;
- end
- -- Set position and return it
- if base == "set" then
- this.pos_ = math.clamp(offset,0, this.len_);
- elseif base == "end" then
- this.pos_ = math.clamp(#this.string_+offset,0, this.len_);
- else -- "cur"
- this.pos_ = math.clamp(this.pos_+offset,0, this.len_);
- end
- return this.pos_;
- end;
- ["setvbuf"] = voidFunc;
- ["write"] = function (this, ...)
- -- Only act on StringWriters
- if not StringWriter_Check(this) then return end;
- -- Concat all arguments (assuming they are valid)
- local s = table.concat({...});
- -- Concat argument string with current string
- if this.pos_ == -1 or this.pos_ == this.len_ then
- -- Just append
- table.insert(this.string_, s);
- else
- -- Insert, merge into a string
- local sFull = table.concat(this.string_);
- -- Split it up
- local sLeft = string.sub(sFull, 1, this.pos_);
- local sRight = string.sub(sFull, this.pos_+1+#s, -1)
- -- And put it back in
- this.string_ = {sLeft, s, sRight};
- end
- -- Update position
- this.pos_ = this.pos_ + #s;
- if this.pos_ > this.len_ then
- this.len_ = this.pos_;
- end;
- end;
- ["get"] = function (this)
- if not StringWriter_Check(this) then
- return nil, "Invalid StringWriter";
- else
- this.string_ = {table.concat(this.string_)};
- return this.string_[1];
- end;
- end;
- }
StringWriter instances are created by a factory method. It initializes the state and sets the metatable.
stringwriter.lua [4.77 kB]
- -- StringWriter factory
- StringWriter = function ()
- local sw = {
- string_ = {""};
- len_ = 0;
- pos_ = 0;
- }
- setmetatable(sw, StringWriter_Meta);
- return sw;
- end
I hope this code is useful for someone, use it as you wish, it is licensed under the MIT license.