runable
This commit is contained in:
371
lib/core/utils/formatters.dart
Normal file
371
lib/core/utils/formatters.dart
Normal file
@@ -0,0 +1,371 @@
|
||||
/// Data Formatters for Vietnamese Locale
|
||||
///
|
||||
/// Provides formatting utilities for currency, dates, phone numbers,
|
||||
/// and other data types commonly used in the app.
|
||||
library;
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Currency formatter for Vietnamese Dong (VND)
|
||||
class CurrencyFormatter {
|
||||
CurrencyFormatter._();
|
||||
|
||||
/// Format amount as Vietnamese currency (e.g., "100,000 ₫")
|
||||
static String format(double amount, {bool showSymbol = true}) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: showSymbol ? '₫' : '',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
/// Format amount with custom precision
|
||||
static String formatWithDecimals(
|
||||
double amount, {
|
||||
int decimalDigits = 2,
|
||||
bool showSymbol = true,
|
||||
}) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: showSymbol ? '₫' : '',
|
||||
decimalDigits: decimalDigits,
|
||||
);
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
/// Format as compact currency (e.g., "1.5M ₫")
|
||||
static String formatCompact(double amount, {bool showSymbol = true}) {
|
||||
final formatter = NumberFormat.compactCurrency(
|
||||
locale: 'vi_VN',
|
||||
symbol: showSymbol ? '₫' : '',
|
||||
decimalDigits: 1,
|
||||
);
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
/// Parse currency string to double
|
||||
static double? parse(String value) {
|
||||
try {
|
||||
// Remove currency symbol and spaces
|
||||
final cleaned = value.replaceAll(RegExp(r'[₫\s,]'), '');
|
||||
return double.tryParse(cleaned);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Date and time formatter
|
||||
class DateFormatter {
|
||||
DateFormatter._();
|
||||
|
||||
/// Format date as "dd/MM/yyyy" (Vietnamese format)
|
||||
static String formatDate(DateTime date) {
|
||||
final formatter = DateFormat('dd/MM/yyyy', 'vi_VN');
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
/// Format date as "dd-MM-yyyy"
|
||||
static String formatDateDash(DateTime date) {
|
||||
final formatter = DateFormat('dd-MM-yyyy', 'vi_VN');
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
/// Format time as "HH:mm"
|
||||
static String formatTime(DateTime date) {
|
||||
final formatter = DateFormat('HH:mm', 'vi_VN');
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
/// Format date and time as "dd/MM/yyyy HH:mm"
|
||||
static String formatDateTime(DateTime date) {
|
||||
final formatter = DateFormat('dd/MM/yyyy HH:mm', 'vi_VN');
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
/// Format date as "dd/MM/yyyy lúc HH:mm"
|
||||
static String formatDateTimeVN(DateTime date) {
|
||||
final formatter = DateFormat('dd/MM/yyyy', 'vi_VN');
|
||||
final timeFormatter = DateFormat('HH:mm', 'vi_VN');
|
||||
return '${formatter.format(date)} lúc ${timeFormatter.format(date)}';
|
||||
}
|
||||
|
||||
/// Format as relative time (e.g., "2 giờ trước")
|
||||
static String formatRelative(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inSeconds < 60) {
|
||||
return 'Vừa xong';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes} phút trước';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours} giờ trước';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays} ngày trước';
|
||||
} else if (difference.inDays < 30) {
|
||||
final weeks = (difference.inDays / 7).floor();
|
||||
return '$weeks tuần trước';
|
||||
} else if (difference.inDays < 365) {
|
||||
final months = (difference.inDays / 30).floor();
|
||||
return '$months tháng trước';
|
||||
} else {
|
||||
final years = (difference.inDays / 365).floor();
|
||||
return '$years năm trước';
|
||||
}
|
||||
}
|
||||
|
||||
/// Format as day of week (e.g., "Thứ Hai")
|
||||
static String formatDayOfWeek(DateTime date) {
|
||||
final formatter = DateFormat('EEEE', 'vi_VN');
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
/// Format as month and year (e.g., "Tháng 10 năm 2024")
|
||||
static String formatMonthYear(DateTime date) {
|
||||
final formatter = DateFormat('MMMM yyyy', 'vi_VN');
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
/// Format as full date with day of week (e.g., "Thứ Hai, 17/10/2024")
|
||||
static String formatFullDate(DateTime date) {
|
||||
final dayOfWeek = formatDayOfWeek(date);
|
||||
final dateStr = formatDate(date);
|
||||
return '$dayOfWeek, $dateStr';
|
||||
}
|
||||
|
||||
/// Parse date string in format "dd/MM/yyyy"
|
||||
static DateTime? parseDate(String dateStr) {
|
||||
try {
|
||||
final formatter = DateFormat('dd/MM/yyyy');
|
||||
return formatter.parse(dateStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse datetime string in format "dd/MM/yyyy HH:mm"
|
||||
static DateTime? parseDateTime(String dateTimeStr) {
|
||||
try {
|
||||
final formatter = DateFormat('dd/MM/yyyy HH:mm');
|
||||
return formatter.parse(dateTimeStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phone number formatter for Vietnamese phone numbers
|
||||
class PhoneFormatter {
|
||||
PhoneFormatter._();
|
||||
|
||||
/// Format phone number as "(0xxx) xxx xxx"
|
||||
static String format(String phone) {
|
||||
// Remove all non-digit characters
|
||||
final cleaned = phone.replaceAll(RegExp(r'\D'), '');
|
||||
|
||||
if (cleaned.isEmpty) return '';
|
||||
|
||||
// Handle Vietnamese phone number formats
|
||||
if (cleaned.startsWith('84')) {
|
||||
// +84 format
|
||||
final local = cleaned.substring(2);
|
||||
if (local.length >= 9) {
|
||||
return '(+84${local.substring(0, 2)}) ${local.substring(2, 5)} ${local.substring(5)}';
|
||||
}
|
||||
} else if (cleaned.startsWith('0')) {
|
||||
// 0xxx format
|
||||
if (cleaned.length >= 10) {
|
||||
return '(${cleaned.substring(0, 4)}) ${cleaned.substring(4, 7)} ${cleaned.substring(7)}';
|
||||
}
|
||||
}
|
||||
|
||||
return phone; // Return original if format doesn't match
|
||||
}
|
||||
|
||||
/// Format as international number (+84xxx xxx xxx)
|
||||
static String formatInternational(String phone) {
|
||||
final cleaned = phone.replaceAll(RegExp(r'\D'), '');
|
||||
|
||||
if (cleaned.isEmpty) return '';
|
||||
|
||||
if (cleaned.startsWith('0')) {
|
||||
// Convert 0xxx to +84xxx
|
||||
final local = cleaned.substring(1);
|
||||
if (local.length >= 9) {
|
||||
return '+84${local.substring(0, 2)} ${local.substring(2, 5)} ${local.substring(5)}';
|
||||
}
|
||||
} else if (cleaned.startsWith('84')) {
|
||||
final local = cleaned.substring(2);
|
||||
if (local.length >= 9) {
|
||||
return '+84${local.substring(0, 2)} ${local.substring(2, 5)} ${local.substring(5)}';
|
||||
}
|
||||
}
|
||||
|
||||
return phone;
|
||||
}
|
||||
|
||||
/// Remove formatting to get clean phone number
|
||||
static String clean(String phone) {
|
||||
return phone.replaceAll(RegExp(r'\D'), '');
|
||||
}
|
||||
|
||||
/// Convert to E.164 format (+84xxxxxxxxx)
|
||||
static String toE164(String phone) {
|
||||
final cleaned = clean(phone);
|
||||
|
||||
if (cleaned.startsWith('0')) {
|
||||
return '+84${cleaned.substring(1)}';
|
||||
} else if (cleaned.startsWith('84')) {
|
||||
return '+$cleaned';
|
||||
} else if (cleaned.startsWith('+84')) {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
return '+84$cleaned';
|
||||
}
|
||||
|
||||
/// Mask phone number (e.g., "0xxx xxx ***")
|
||||
static String mask(String phone) {
|
||||
final cleaned = clean(phone);
|
||||
|
||||
if (cleaned.length >= 10) {
|
||||
return '${cleaned.substring(0, 4)} ${cleaned.substring(4, 7)} ***';
|
||||
}
|
||||
|
||||
return phone;
|
||||
}
|
||||
}
|
||||
|
||||
/// Number formatter
|
||||
class NumberFormatter {
|
||||
NumberFormatter._();
|
||||
|
||||
/// Format number with thousand separators
|
||||
static String format(num number, {int decimalDigits = 0}) {
|
||||
final formatter = NumberFormat('#,###', 'vi_VN');
|
||||
if (decimalDigits > 0) {
|
||||
return formatter.format(number);
|
||||
}
|
||||
return formatter.format(number.round());
|
||||
}
|
||||
|
||||
/// Format as percentage
|
||||
static String formatPercentage(
|
||||
double value, {
|
||||
int decimalDigits = 0,
|
||||
bool showSymbol = true,
|
||||
}) {
|
||||
final formatter = NumberFormat.percentPattern('vi_VN');
|
||||
formatter.maximumFractionDigits = decimalDigits;
|
||||
formatter.minimumFractionDigits = decimalDigits;
|
||||
|
||||
final result = formatter.format(value / 100);
|
||||
return showSymbol ? result : result.replaceAll('%', '');
|
||||
}
|
||||
|
||||
/// Format as compact number (e.g., "1.5K")
|
||||
static String formatCompact(num number) {
|
||||
final formatter = NumberFormat.compact(locale: 'vi_VN');
|
||||
return formatter.format(number);
|
||||
}
|
||||
|
||||
/// Format file size
|
||||
static String formatBytes(int bytes, {int decimals = 2}) {
|
||||
if (bytes <= 0) return '0 B';
|
||||
|
||||
const suffixes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
final i = (bytes.bitLength - 1) ~/ 10;
|
||||
final value = bytes / (1 << (i * 10));
|
||||
|
||||
return '${value.toStringAsFixed(decimals)} ${suffixes[i]}';
|
||||
}
|
||||
|
||||
/// Format duration (e.g., "1:30:45")
|
||||
static String formatDuration(Duration duration) {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text formatter utilities
|
||||
class TextFormatter {
|
||||
TextFormatter._();
|
||||
|
||||
/// Capitalize first letter
|
||||
static String capitalize(String text) {
|
||||
if (text.isEmpty) return text;
|
||||
return text[0].toUpperCase() + text.substring(1);
|
||||
}
|
||||
|
||||
/// Capitalize each word
|
||||
static String capitalizeWords(String text) {
|
||||
if (text.isEmpty) return text;
|
||||
return text.split(' ').map((word) => capitalize(word)).join(' ');
|
||||
}
|
||||
|
||||
/// Truncate text with ellipsis
|
||||
static String truncate(String text, int maxLength, {String ellipsis = '...'}) {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - ellipsis.length) + ellipsis;
|
||||
}
|
||||
|
||||
/// Remove diacritics from Vietnamese text
|
||||
static String removeDiacritics(String text) {
|
||||
const withDiacritics = 'àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ';
|
||||
const withoutDiacritics = 'aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiioooooooooooooooooouuuuuuuuuuuyyyyyd';
|
||||
|
||||
var result = text.toLowerCase();
|
||||
for (var i = 0; i < withDiacritics.length; i++) {
|
||||
result = result.replaceAll(withDiacritics[i], withoutDiacritics[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Create URL-friendly slug
|
||||
static String slugify(String text) {
|
||||
var slug = removeDiacritics(text);
|
||||
slug = slug.toLowerCase();
|
||||
slug = slug.replaceAll(RegExp(r'[^\w\s-]'), '');
|
||||
slug = slug.replaceAll(RegExp(r'[-\s]+'), '-');
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
|
||||
/// Loyalty tier formatter
|
||||
class LoyaltyFormatter {
|
||||
LoyaltyFormatter._();
|
||||
|
||||
/// Format tier name in Vietnamese
|
||||
static String formatTier(String tier) {
|
||||
switch (tier.toLowerCase()) {
|
||||
case 'diamond':
|
||||
return 'Kim Cương';
|
||||
case 'platinum':
|
||||
return 'Bạch Kim';
|
||||
case 'gold':
|
||||
return 'Vàng';
|
||||
default:
|
||||
return tier;
|
||||
}
|
||||
}
|
||||
|
||||
/// Format points with label
|
||||
static String formatPoints(int points) {
|
||||
return '${NumberFormatter.format(points)} điểm';
|
||||
}
|
||||
|
||||
/// Format points progress (e.g., "1,200 / 5,000 điểm")
|
||||
static String formatPointsProgress(int current, int target) {
|
||||
return '${NumberFormatter.format(current)} / ${NumberFormatter.format(target)} điểm';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user