diff --git a/decimal.go b/decimal.go
index c614ea79..692763e0 100644
--- a/decimal.go
+++ b/decimal.go
@@ -1025,6 +1025,73 @@ func (d Decimal) String() string {
 	return d.string(true)
 }
 
+// Format formats a decimal.
+// thousandsSeparator can be empty, in which case the integer value will be displayed without separation.
+// if decimalSeparator is empty and the value is a decimal this will panic.
+func (d Decimal) Format(thousandsSeparator string, decimalSeparator string, trimTrailingZeros bool) string {
+	if d.exp >= 0 {
+		d = d.rescale(0)
+	}
+
+	abs := new(big.Int).Abs(d.value)
+	str := abs.String()
+
+	var intPart, fractionalPart string
+
+	// NOTE(vadim): this cast to int will cause bugs if d.exp == INT_MIN
+	// and you are on a 32-bit machine. Won't fix this super-edge case.
+	dExpInt := int(d.exp)
+	if len(str) > -dExpInt {
+		intPart = str[:len(str)+dExpInt]
+		fractionalPart = str[len(str)+dExpInt:]
+	} else {
+		intPart = "0"
+
+		num0s := -dExpInt - len(str)
+		fractionalPart = strings.Repeat("0", num0s) + str
+	}
+
+	if thousandsSeparator != "" {
+		parts := 1 + (len(intPart)-1)/3
+		if parts > 1 {
+			intParts := make([]string, 1+(len(intPart)-1)/3)
+			offset := len(intPart) - (len(intParts)-1)*3
+			for i := 0; i < len(intParts); i++ {
+				if i == 0 {
+					intParts[i] = intPart[0:offset]
+				} else {
+					intParts[i] = intPart[(i-1)*3+offset : i*3+offset]
+				}
+			}
+			intPart = strings.Join(intParts, thousandsSeparator)
+		}
+	}
+
+	if trimTrailingZeros {
+		i := len(fractionalPart) - 1
+		for ; i >= 0; i-- {
+			if fractionalPart[i] != '0' {
+				break
+			}
+		}
+		fractionalPart = fractionalPart[:i+1]
+	}
+	if fractionalPart != "" && decimalSeparator == "" {
+		panic("no decimal separator for non-integer")
+	}
+
+	number := intPart
+	if len(fractionalPart) > 0 {
+		number += decimalSeparator + fractionalPart
+	}
+
+	if d.value.Sign() < 0 {
+		return "-" + number
+	}
+
+	return number
+}
+
 // StringFixed returns a rounded fixed-point string with places digits after
 // the decimal point.
 //
@@ -1461,48 +1528,7 @@ func (d Decimal) StringScaled(exp int32) string {
 }
 
 func (d Decimal) string(trimTrailingZeros bool) string {
-	if d.exp >= 0 {
-		return d.rescale(0).value.String()
-	}
-
-	abs := new(big.Int).Abs(d.value)
-	str := abs.String()
-
-	var intPart, fractionalPart string
-
-	// NOTE(vadim): this cast to int will cause bugs if d.exp == INT_MIN
-	// and you are on a 32-bit machine. Won't fix this super-edge case.
-	dExpInt := int(d.exp)
-	if len(str) > -dExpInt {
-		intPart = str[:len(str)+dExpInt]
-		fractionalPart = str[len(str)+dExpInt:]
-	} else {
-		intPart = "0"
-
-		num0s := -dExpInt - len(str)
-		fractionalPart = strings.Repeat("0", num0s) + str
-	}
-
-	if trimTrailingZeros {
-		i := len(fractionalPart) - 1
-		for ; i >= 0; i-- {
-			if fractionalPart[i] != '0' {
-				break
-			}
-		}
-		fractionalPart = fractionalPart[:i+1]
-	}
-
-	number := intPart
-	if len(fractionalPart) > 0 {
-		number += "." + fractionalPart
-	}
-
-	if d.value.Sign() < 0 {
-		return "-" + number
-	}
-
-	return number
+	return d.Format("", ".", trimTrailingZeros)
 }
 
 func (d *Decimal) ensureInitialized() {
diff --git a/decimal_test.go b/decimal_test.go
index 2b3a99e1..b19802c7 100644
--- a/decimal_test.go
+++ b/decimal_test.go
@@ -1464,6 +1464,47 @@ func TestDecimal_RoundDownAndStringFixed(t *testing.T) {
 	}
 }
 
+func TestDecimal_Format(t *testing.T) {
+	type testData struct {
+		input              string
+		thousandsSeparator string
+		decimalSeparator   string
+		trimTrailingZeros  bool
+		expected           string
+	}
+	tests := []testData{
+		{"0", ",", ".", false, "0"},
+		{"0", ",", ".", true, "0"},
+		{"999", ",", ".", true, "999"},
+		{"1000", ",", ".", true, "1,000"},
+		{"123", ",", ".", true, "123"},
+		{"1234", ",", ".", true, "1,234"},
+		{"12345.67", "", ".", true, "12345.67"},
+		{"12345.00", ",", ".", true, "12,345"},
+		{"12345.00", ",", ".", false, "12,345.00"},
+		{"123456.00", ",", ".", false, "123,456.00"},
+		{"1234567.00", ",", ".", false, "1,234,567.00"},
+		{"1234567.00", ".", ",", false, "1.234.567,00"},
+		{"1234567.00", "_", ".", true, "1_234_567"},
+		{"-12.00", "_", ".", true, "-12"},
+		{"-123.00", "_", ".", true, "-123"},
+		{"-1234.00", "_", ".", true, "-1_234"},
+	}
+
+	for _, test := range tests {
+		d, err := NewFromString(test.input)
+		if err != nil {
+			panic(err)
+		}
+
+		got := d.Format(test.thousandsSeparator, test.decimalSeparator, test.trimTrailingZeros)
+		if got != test.expected {
+			t.Errorf("Format %s got %s, expected %s",
+				d, got, test.expected)
+		}
+	}
+}
+
 func TestDecimal_BankRoundAndStringFixed(t *testing.T) {
 	type testData struct {
 		input         string