package gophplib
import (
"encoding/base64"
"fmt"
"math"
"reflect"
)
// Base64Encode emulates the functionality of PHP 5.6's base64_encode function.
// For more information, see the [official PHP documentation].
// In PHP 5.6, an error is triggered if the input size is excessively large due to memory limitations.
// This Go implementation includes similar checks to emulate PHP's memory limitation conditions.
// Additionally, this function converts different types of variables to a string, following PHP's dynamic typing approach.
// After ensuring the memory constraints are met and converting the input to a string,
// it uses the EncodeToString function from the encoding/base64 package to perform the Base64 encoding.
// The result is a Base64 encoded string, consistent with PHP's output for the same input.
// For more detailed information about the EncodeToString function in the package encoding/base64,
// see the [encoding/base64's EncodeToString documentation]
//
// This function returns error if given argument is not one of following:
// string, int, int64, float64, bool, nil, and any type which does not implement
// interface { toString() string }.
//
// PHP references:
// - base64_encode definition:
// https://github.com/php/php-src/blob/php-5.6.40/ext/standard/base64.c#L224-L241
// - base64_encode implementation:
// https://github.com/php/php-src/blob/4b8f72da5dfb201af4e82dee960261d8657e414f/ext/standard/base64.c#L56-L106
//
// Test Cases :
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_basic_001.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_basic_002.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_error_001.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/url/base64_encode_variation_001.phpt
//
// [official PHP documentation]: https://www.php.net/manual/en/function.base64-encode
// [encoding/base64's EncodeToString documentation]: https://pkg.go.dev/encoding/base64#Encoding.EncodeToString
func Base64Encode(value any) (string, error) {
// Convert a value to string
characterString, err := zendParseArgAsString(value)
if err != nil {
return "", fmt.Errorf("unsupported type : %s", reflect.TypeOf(value))
}
// Base64 encoding converts 3 bytes into 4 bytes ASCII characters,
// and adds padding 2 bytes if the converted data is not a multiple of 3.
// In PHP, the base64_encode function includes a memory allocation limit check
// to prevent potential overflow issues due to the increase in data size during encoding.
// In Go, such checks are generally not required thanks to its robust memory management system.
// However, to maintain exact behavioral parity with the PHP implementation,
// this function includes memory limit check too.
if (len(characterString)+2)/3 > math.MaxInt32/4 {
return "", fmt.Errorf("string too long, maximum is 1610612733")
}
encodedString := base64.StdEncoding.EncodeToString([]byte(characterString))
return encodedString, nil
}
package gophplib
import (
"fmt"
"reflect"
"strings"
"github.com/elliotchance/orderedmap/v2"
)
// Implode replicates the behavior of PHP 5.6's implode function in GO.
// This function concatenates the elements of an array into a single string using a specified separator.
// For more information, see the [official PHP documentation].
//
// The function supports flexible argument patterns to accommodate various use cases.
// - arg1: Can be either a string (used as the separator) or an array of elements to be joined.
// - options: Optional. When provided, the first element is used as arg2.
// If arg1 is a string, arg2 serves as the array of elements to be joined.
// If arg1 is an array, arg2 serves as the separator.
//
// Behavior:
//
// 1. If only arg1 is provided:
// - If arg1 is an array, the function joins the elements using an empty string as the default separator.
// - If arg1 is not an array, the function returns an error.
//
// 2. If both arg1 and arg2 are provided:
// - If arg1 is an array, arg2 is converted to string and used as the separator.
// - If arg1 is not an array and arg2 is an array, arg1 is converted to a string
// and used as the separator, with arg2 being the array to implode.
// - If neither arg1 nor arg2 is an array, the function returns an error.
//
// Non-string elements within the array are converted to strings using a ConvertToString function
// before joining.
// Due to language differences between PHP and Go, the implode function support OrderedMap type from the [orderedmap library],
// ensuring ordered map functionality. When imploding map types, please utilize the OrderedMap type from the [orderedmap library]
// to maintain element order. If you use map type, not OrderedMap type, the order of the results cannot be guaranteed.
//
// reference:
// - implode: https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L1229-L1269
// - php_implode: https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L1141-L1224
//
// Test cases:
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/implode.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/implode1.phpt
//
// [official PHP documentation]: https://www.php.net/manual/en/function.implode.php
// [orderedmap library]: https://pkg.go.dev/github.com/elliotchance/orderedmap/v2
func Implode(arg1 any, options ...any) (string, error) {
var delim string
var arr []any
// Check arg1 is one of array, slice, map, or ordered ap
isArg1CollectionType := isCollectionType(arg1)
// Check if options is not provided
if len(options) == 0 {
if !isArg1CollectionType {
return "", fmt.Errorf("argument must be one of array, slice, or ordered map, but got %v", reflect.TypeOf(arg1))
}
arr = aggregateValues(arg1)
} else {
arg2 := options[0]
// Check arg2 is one of array, slice, or ordered map
isArg2CollectionType := isCollectionType(arg2)
if isArg1CollectionType {
delim, _ = ConvertToString(arg2)
arr = aggregateValues(arg1)
} else if !isArg1CollectionType && isArg2CollectionType {
delim, _ = ConvertToString(arg1)
arr = aggregateValues(arg2)
} else {
return "", fmt.Errorf("invalid arguments passed, got %v, %v", reflect.TypeOf(arg1), reflect.TypeOf(arg2))
}
}
// Join arr elements with a delim
var builder strings.Builder
if len(arr) == 0 {
return "", nil
}
for i, item := range arr {
str, err := ConvertToString(item)
if err != nil {
return "", fmt.Errorf("unsupported type in array : %v", reflect.TypeOf(item))
} else {
builder.WriteString(str)
}
if i < len(arr)-1 {
builder.WriteString(delim)
}
}
return builder.String(), nil
}
// isOrderedMap checks if the argument is an instance of ordered map
func isOrderedMap(arg any) bool {
switch arg.(type) {
case orderedmap.OrderedMap[any, any], *orderedmap.OrderedMap[any, any]:
return true
default:
return false
}
}
// isCollectionType checks if the argument is either an array, a slice, a map or ordered map
func isCollectionType(arg any) bool {
if arg == nil {
return false
}
argType := reflect.TypeOf(arg).Kind()
return isOrderedMap(arg) || argType == reflect.Slice || argType == reflect.Array || argType == reflect.Map
}
// aggregateValues extracts the stored value from different types of source:
// ordered map, map, slice and array. It gathers there values into an arr and returns it.
func aggregateValues(source any) []any {
if isOrderedMap(source) {
var om *orderedmap.OrderedMap[any, any]
switch tmp := source.(type) {
case orderedmap.OrderedMap[any, any]:
// If source is an OrderedMap struct, use address of source
om = &tmp
case *orderedmap.OrderedMap[any, any]:
om = tmp
}
arr := make([]any, 0, om.Len())
for el := om.Front(); el != nil; el = el.Next() {
arr = append(arr, el.Value)
}
return arr
} else {
v := reflect.ValueOf(source)
arr := make([]any, 0, v.Len())
switch v.Kind() {
case reflect.Map:
for _, value := range v.MapKeys() {
arr = append(arr, v.MapIndex(value).Interface())
}
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
arr = append(arr, v.Index(i).Interface())
}
}
return arr
}
}
package gophplib
import (
"fmt"
"reflect"
)
// Ord is a ported functions that works exactly the same as PHP 5.6's ord function.
// In PHP 5.6, when the ord() function is used with a data type other
// than a string, it automatically converts the given variable into a string
// before processing it. To achieve the same behavior in Go,
// this function converts an argument to string using the zendParseArgAsString() function.
// For more information, see the [official PHP documentation].
//
// This function returns error if given argument is not one of following:
// string, int, int64, float64, bool, nil, and any type which does not implement
// interface { toString() string }.
//
// Reference :
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L2666-L2676
//
// Test Cases:
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/ord_basic.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/ord_error.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/ord_variation1.phpt
//
// [official PHP documentation]: https://www.php.net/manual/en/function.ord.php
func Ord(character any) (byte, error) {
// Convert a character to string
characterString, err := zendParseArgAsString(character)
if err != nil {
return 0, fmt.Errorf("unsupported type : %s", reflect.TypeOf(character))
}
// Check if the characterString is not empty
if len(characterString) > 0 {
// Return the first byte of input argument's string representation
return []byte(characterString)[0], nil
}
// Return for empty strings
return 0, nil
}
package gophplib
import (
"bytes"
"strconv"
"strings"
"github.com/elliotchance/orderedmap/v2"
)
// ParseStr is a ported function that works exactly the same as PHP's parse_str
// function. For more information, see the [official PHP documentation].
//
// All keys of the returned orderedmap.OrderedMap are either string or int. Type of all values of
// the returned orderedmap.OrderedMap are either string or "orderedmap.OrderedMap[string | int]RetVal".
// For more information about orderedmap libaray, see the [orderedmap documentation].
//
// Reference:
// - https://www.php.net/manual/en/function.parse-str.php
// - https://github.com/php/php-src/blob/php-5.6.40/main/php_variables.c#L450-L496
// - https://github.com/php/php-src/blob/php-8.3.0/main/php_variables.c#L523-L568
//
// [official PHP documentation]: https://www.php.net/manual/en/function.parse-str.php
// [orderedmap documentation]: https://pkg.go.dev/github.com/elliotchance/orderedmap/v2@v2.2.0
func ParseStr(input string) orderedmap.OrderedMap[any, any] {
ret := newPHPArray()
// Split input with '&'
pairs := strings.Split(input, "&")
for _, pair := range pairs {
// Skip empty pair
if pair == "" {
continue
}
// Cut pair with '='
key, value, _ := strings.Cut(pair, "=")
registerVariableSafe(Urldecode(key), Urldecode(value), ret)
}
return ret.intoMap()
}
// registerVariableSafe is a ported function that works exactly the same as
// PHP's php_register_variable_safe function.
//
// Reference:
// - https://github.com/php/php-src/blob/php-5.6.40/main/php_variables.c#L59-L233
// - https://github.com/php/php-src/blob/php-8.3.0/main/php_variables.c#L90-L314
func registerVariableSafe(key, value string, track *phpSymtable) {
// NOTE: key is "var_name", value is "val", track is "track_vars_array" in
// below PHP version's function signature.
//
// PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *track_vars_array)
// ignore leading spaces in the variable name
key = strings.TrimLeft(key, " ")
// Prepare variable name
// NOTE: key_new is "var" and "var_orig" in the original PHP codes.
key_new := []byte(key)
// ensure that we don't have spaces or dots in the variable name (not binary safe)
is_array := false
index_slice := []byte(nil) // index_slice is "ip" in the original PHP codes.
for i, c := range key_new {
if c == ' ' || c == '.' {
key_new[i] = '_'
} else if c == '[' {
is_array = true
key_new, index_slice = key_new[:i], key_new[i:]
break
}
}
// empty variable name, or variable name with a space in it
if len(key_new) == 0 {
return
}
index := key_new
if is_array {
// We do not perform max nesting level check here
idx := 0 // idx is offset of "ip" pointer in the original PHP codes.
for {
idx++
idx_s := idx // idx_next is "index_s" in the original PHP codes.
if isAsciiWhitespace(index_slice[idx]) {
idx++
}
if index_slice[idx] == ']' {
idx_s = -1
} else {
ret := bytes.IndexByte(index_slice[idx:], ']')
if ret == -1 {
// not an index; un-terminate the var name
index_slice[idx_s-1] = '_'
// NOTE: concat of `index` and `index_slice[idx_s-1:]` only
// occurs when idx_s == 1.
if index != nil && idx_s == 1 {
index = append(index, index_slice...)
}
goto plain_var
}
idx += ret
}
var subdict *phpSymtable
if index == nil {
subdict = newPHPArray()
track.setNext(subdict)
} else {
value, ok := track.get(index)
if !ok {
subdict = newPHPArray()
track.set(index, subdict)
} else {
// References for origianl PHP codes of here:
// - https://www.phpinternalsbook.com/php7/zvals/memory_management.html
// - https://www.phpinternalsbook.com/php7/zvals/basic_structure.html
underlying, ok := value.(*phpSymtable)
if !ok {
subdict = newPHPArray()
track.set(index, subdict)
} else {
subdict = underlying
}
}
}
track = subdict
if idx_s != -1 {
index = index_slice[idx_s:idx]
} else {
index = nil
}
idx++
if idx < len(index_slice) && index_slice[idx] == '[' {
// Do nothing
} else {
goto plain_var
}
}
}
plain_var:
if index == nil {
track.setNext(value)
} else {
track.set(index, value)
}
}
// phpSymtable is a orderedmap.OrderedMap[any, any] which behaves like PHP's array. It maintains
// internal next (i.e. nNextFreeElement of PHP) state and it automatically
// converts numeric string keys to integer keys.
type phpSymtable struct {
next int
// Key is either string or int.
// Value is either string or *phpSymtable.
d orderedmap.OrderedMap[any, any]
}
func newPHPArray() *phpSymtable {
return &phpSymtable{
next: 0,
d: *orderedmap.NewOrderedMap[any, any](),
}
}
// It returns an orderedmap.OrderedMap[any, any] whose keys are either string or int, and whose
// values (RetVal) are either string or "orderedmap.OrderedMap[string | int, RetVal]".
func (p *phpSymtable) intoMap() orderedmap.OrderedMap[any, any] {
ret := *orderedmap.NewOrderedMap[any, any]()
for el := p.d.Front(); el != nil; el = el.Next() {
key := el.Key
value := el.Value
if sub, ok := el.Value.(*phpSymtable); ok {
ret.Set(key, sub.intoMap())
} else {
ret.Set(key, value)
}
}
return ret
}
func (p *phpSymtable) get(key []byte) (any, bool) {
k := phpNumericOrString(key)
v, ok := p.d.Get(k)
return v, ok
}
func (p *phpSymtable) set(key []byte, value any) {
k := phpNumericOrString(key)
if numeric, ok := k.(int); ok {
p.next = maxInt(p.next, numeric+1)
}
p.d.Set(k, value)
}
func (p *phpSymtable) setNext(value any) {
p.d.Set(p.next, value)
p.next++
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func phpNumericOrString(input []byte) any {
str := string(input)
if !zendHandleNumericStr(str) {
return str
}
num, err := strconv.Atoi(str)
// Check if it overflows
if err != nil {
return str
}
return num
}
// zendHandleNumericStr is a ported function that works exactly the same as
// PHP's _zend_handle_numeric_str function.
//
// It returns true if the input string meets all the following conditions:
// - It is a signed integer string without leading zeros. (positive sign is
// not allowed, only negative sign is allowed)
// - It is not a negative zero.
//
// It behaves same with regexp.MustCompile(`^-?[1-9][0-9]*$|^0$`).MatchString
//
// References:
// - https://github.com/php/php-src/blob/php-8.3.0/Zend/zend_hash.h#L388-L404
// - https://github.com/php/php-src/blob/php-8.3.0/Zend/zend_hash.c#L3262-L3299
func zendHandleNumericStr(s string) bool {
// Handle few cases first to make further checks simpler
switch s {
case "0":
return true
case "", "-":
return false
}
// Check for negative sign
begin := 0
if s[0] == '-' {
begin = 1
}
// Ensure the first character isn't '0'
if s[begin] == '0' {
return false
}
// Check that all characters are digits
for _, ch := range s[begin:] {
if ch < '0' || ch > '9' {
return false
}
}
return true
}
// isAsciiWhitespace is an ASCII-only version of C's isspace.
//
// References:
// - https://en.cppreference.com/w/c/string/byte/isspace
func isAsciiWhitespace(c byte) bool {
return c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\v'
}
package gophplib
import (
"fmt"
"reflect"
)
// Strlen is a ported function that works exactly the same as PHP 5.6's strlen function.
// In PHP 5.6, when the strlen() function is used with a data type other
// than a string, it automatically converts the given variable into a string
// before processing it. To achieve the same behavior in Go,
// this function converts an argument to string using the zendParseArgAsString() function.
// For more information, see the [official PHP documentation].
//
// This function returns error if given argument is not one of following:
// string, int, int64, float64, bool, nil, and any type which does not implement
// interface { toString() string }.
//
// Reference :
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_builtin_functions.c#L479-L492
//
// Test Case :
// - https://github.dev/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen_variation1.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen_error.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/strlen_basic.phpt
//
// [official PHP documentation]: https://www.php.net/manual/en/function.strlen.php
func Strlen(value any) (int, error) {
// Convert a value to string
characterString, err := zendParseArgAsString(value)
if err != nil {
return 0, fmt.Errorf("unsupported type : %s", reflect.TypeOf(value))
}
return len(characterString), nil
}
package gophplib
import (
"fmt"
"reflect"
"strings"
)
// Trim is a ported function that works exactly the same as PHP 5.6's trim
// function. For more information, see the [official PHP documentation].
//
// In PHP 5.6, when attempting to use the trim() function with a data type other
// than a string, it automatically converts the requested variable into a string
// before performing the trim. To achieve the same behavior in Go, this function
// converts the requested data types into strings and then utilize the trim
// function from the package strings. For more detailed information about the
// trim function in the package strings, see the [strings's trim documentation]
//
// This function returns error if given argument is not one of following:
// string, int, int64, float64, bool, nil, and any type which does not implement
// interface { toString() string }.
//
// NOTE: This function does not support the second parameter of original parse_str yet.
// It only strips the default characters (" \n\r\t\v\x00")
//
// References:
// - https://www.php.net/manual/en/function.trim.php
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/string.c#L840-L850
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L425-L470
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L593-L661
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L261-L302
//
// Test Cases:
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim1.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim_basic.phpt
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/tests/strings/trim_variation1.phpt
//
// [official PHP documentation]: https://www.php.net/manual/en/function.trim.php
// [strings's trim documentation]: https://pkg.go.dev/strings#Trim
func Trim(value any) (ret string, err error) {
// Convert a value to string
characterString, err := zendParseArgAsString(value)
if err != nil {
err = fmt.Errorf("unsupported type : %s", reflect.TypeOf(value))
return
}
const charSet = " \n\r\t\v\x00"
ret = strings.Trim(characterString, charSet)
return
}
package gophplib
import (
"strings"
)
// Urldecode is a ported function that works exactly the same as PHP's urldecode
// function. For more information, see the [official PHP documentation].
//
// Unlike net/url's QueryUnescape function, this function *never* fails. Instead
// of returning an error, it leaves invalid percent encoded sequences as is.
// And contrary to its name, it does not follow percent encoding specification
// of RFC 3986 since it decodes '+' to ' '. This is done to be compatible with
// PHP's urldecode function.
//
// References:
// - https://www.php.net/manual/en/function.urldecode.php
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/url.c#L513-L561
// - https://github.com/php/php-src/blob/php-8.3.0/ext/standard/url.c#L578-L618
//
// [official PHP documentation]: https://www.php.net/manual/en/function.urldecode.php
func Urldecode(input string) string {
buf := []byte(input)
length := len(buf)
j := 0
for i := 0; i < length; i, j = i+1, j+1 {
if buf[i] == '+' {
buf[j] = ' '
} else if buf[i] == '%' && i+2 < length && isxdigit(buf[i+1]) && isxdigit(buf[i+2]) {
buf[j] = htoi(buf[i+1], buf[i+2])
i += 2
} else {
buf[j] = buf[i]
}
}
return strings.ToValidUTF8(string(buf[:j]), "�")
}
// isxdigit is a ported function that works exactly the same as C's isxdigit
// function.
//
// References:
// - https://en.cppreference.com/w/c/string/byte/isxdigit
func isxdigit(c byte) bool {
return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
}
// htoi is a ported function that works exactly the same as PHP's php_htoi
// function. It returns the byte value of the hexadecimal number represented by
// the two bytes hi and lo.
//
// It expects both hi and lo to be valid hexadecimal digits. (ex: '0'-'9',
// 'a'-'f', 'A'-'F') Otherwise, the result is undefined.
//
// References:
// - https://github.com/php/php-src/blob/php-8.3.0/ext/standard/url.c#L426-L444
// - https://github.com/php/php-src/blob/php-5.6.40/ext/standard/url.c#L407-L426
func htoi(hi, lo byte) byte {
if 'A' <= hi && hi <= 'Z' {
hi += 'a' - 'A'
}
if '0' <= hi && hi <= '9' {
hi -= '0'
} else {
hi -= 'a' - 10
}
if 'A' <= lo && lo <= 'Z' {
lo += 'a' - 'A'
}
if '0' <= lo && lo <= '9' {
lo -= '0'
} else {
lo -= 'a' - 10
}
return hi*16 + lo
}
package gophplib
import (
"fmt"
"reflect"
)
// zendParseArgAsString attempts to replicate the behavior of the 'zend_parse_arg_impl' function
// from PHP 5.6, specifically for the case where the 'spec' parameter is "s".
// It handles conversion of different types to string in a way that aligns with PHP's type juggling rules,
// calling ConvertToString to manage string, int, float, and bool types, akin to PHP's _convert_to_string.
//
// This function returns error if given argument is not one of following:
// string, int, int8, int16, int32, int64, float32, float64, bool, nil
// and any type which does not implement interface { toString() string }.
//
// Reference :
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L685-L713
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L425-L470
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L593-L661
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_API.c#L261-L301
func zendParseArgAsString(value any) (string, error) {
var str string
switch v := value.(type) {
case string, int, int8, int16, int32, int64, float32, float64, bool:
return ConvertToString(value)
case nil:
// TODO: handle check_null
str = ""
case toStringAble:
// For types implementing toString(), get the value of toString()
str = v.toString()
default:
return "", fmt.Errorf("unsupported type : %s", reflect.TypeOf(v))
}
return str, nil
}
package gophplib
import (
"database/sql"
"fmt"
"math"
"net"
"os"
"reflect"
)
type toStringAble interface {
toString() string
}
// floatToString converts a float64 to a string based on the PHP 5.6 rules.
// - Allows up to a maximum of 14 digits, including both integer and decimal places.
// - Remove trailing zeros from the fractional part
// ex) 123.4000 → "123.4"
// - Keep the values as is if the last digit is not 0.
// ex) 123.45 → "123.45"
// - If the integer part exceeds 14 digits, use exponential notation.
// ex) 123456789123456.40 → "1.2345678901234e+14"
// - If the total number of digits exceeds 14, truncate the decimal places.
// ex) 123.45678901234 → "123.4567890123"
//
// Reference :
// - https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L627-L633
func floatToString(f64 float64) string {
if math.IsNaN(f64) {
return "NAN"
}
if math.IsInf(f64, 1) {
return "INF"
}
if math.IsInf(f64, -1) {
return "-INF"
}
return fmt.Sprintf("%.*G", 14, f64)
}
// ConvertToString attempts to convert the given value to string, emulating PHP 5.6'S _convert_to_string behavior.
// Unlike PHP, which has built-in support for managing resource IDs for types like files and database connections,
// Go does not inherently manage resource IDs. Due to this language difference, this function uses the values' pointer
// address as the pseudo resource ID for identifiable resource types.
//
// This function returns error if given argument is not one of following:
// string, int, int8, int16, int32, int64, float32, float64, bool, nil, *os.File, *net.Conn, and *sql.DB,
// array, slice, map and any type which does not implement interface { toString() string }.
//
// NOTE : If the given argument's type is float32, it will be converted to float64 internally.
// However, converting float32 to float64 may lead to precision loss.
// Therefore, using float64 is recommended for higher accuracy.
//
// Reference:
// - _convert_to_string implementation:
// https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L593-L661
// - convert_object_to_type implementation:
// https://github.com/php/php-src/blob/php-5.6.40/Zend/zend_operators.c#L333-L357
func ConvertToString(value any) (string, error) {
if value == nil {
return "", nil
}
// handle basic and composite types dynamically
switch v := value.(type) {
case string:
return v, nil
case bool:
if v {
return "1", nil
} else {
return "", nil
}
case int, int8, int16, int32, int64:
return fmt.Sprintf("%d", v), nil
case float32:
return floatToString(float64(v)), nil
case float64:
return floatToString(v), nil
// check for special types such as a pointer of file, network, database resources
case *os.File, *net.Conn, *sql.DB:
// using a resource's address as the resource ID
return fmt.Sprintf("Resource id %p", v), nil
}
// use reflection to handle array, slice, map types
t := reflect.ValueOf(value).Kind()
if t == reflect.Array || t == reflect.Slice || t == reflect.Map {
return "Array", nil
}
if t == reflect.Struct {
if result, ok := value.(toStringAble); ok {
return result.toString(), nil
}
}
// return an error for unsupported types.
return "", fmt.Errorf("unsupported type : %T", value)
}