在这里插入图片描述

今天我们用 React Native 实现一个罗马数字转换工具,支持阿拉伯数字和罗马数字的双向转换,还带有常用数字对照表。

罗马数字转换核心算法

import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';

export const RomanNumeral: React.FC = () => {
  const [arabic, setArabic] = useState('');
  const [roman, setRoman] = useState('');
  
  const buttonAnim1 = useRef(new Animated.Value(1)).current;
  const buttonAnim2 = useRef(new Animated.Value(1)).current;
  const resultAnim = useRef(new Animated.Value(1)).current;
  const exampleAnims = useRef(Array(8).fill(0).map(() => new Animated.Value(0))).current;

  const toRoman = (num: number): string => {
    if (num <= 0 || num > 3999) return '范围: 1-3999';
    const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
    const symbols = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
    let result = '';
    for (let i = 0; i < values.length; i++) {
      while (num >= values[i]) { result += symbols[i]; num -= values[i]; }
    }
    return result;
  };

罗马数字转换的核心是贪心算法。

范围检查:罗马数字只能表示 1-3999 的整数。超出范围返回提示信息。为什么是 3999?因为罗马数字没有表示 4000 及以上的标准符号,最大的符号是 M(1000),3999 是 MMMCMXCIX。

值和符号数组

  • values 数组存储所有可能的值,从大到小排列
  • symbols 数组存储对应的罗马符号
  • 两个数组索引一一对应,比如 values[0] 是 1000,symbols[0] 是 ‘M’

为什么包含 900、400、90、40、9、4 这些特殊值?因为罗马数字有减法规则:小数字在大数字前面表示减法。比如 IV 是 4(5-1),IX 是 9(10-1),XL 是 40(50-10),XC 是 90(100-10),CD 是 400(500-100),CM 是 900(1000-100)。如果不包含这些特殊值,4 会被转换成 IIII,9 会被转换成 VIIII,这不符合罗马数字的标准写法。

贪心算法:从最大的值开始,能用就用,用完再用下一个。while (num >= values[i]) 循环不断减去当前值,直到不够减为止。比如 2024,先减 1000 两次得到 MM,剩 24,再减 10 两次得到 XX,剩 4,最后减 4 一次得到 IV,结果是 MMXXIV。

为什么用贪心算法?因为罗马数字的规则就是"尽量用大的符号"。贪心算法保证每一步都选择最大的可用符号,最终得到最短的罗马数字表示。

罗马数字转阿拉伯数字

  const toArabic = (s: string): number => {
    const map: { [key: string]: number } = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 };
    let result = 0;
    const str = s.toUpperCase();
    for (let i = 0; i < str.length; i++) {
      const curr = map[str[i]] || 0;
      const next = map[str[i + 1]] || 0;
      if (curr < next) result -= curr;
      else result += curr;
    }
    return result;
  };

罗马数字转阿拉伯数字的算法更简单,核心是处理减法规则。

符号映射表:用对象存储 7 个基本罗马符号和对应的值。I=1, V=5, X=10, L=50, C=100, D=500, M=1000。

转大写toUpperCase() 把输入转成大写,因为罗马数字不区分大小写,但标准写法是大写。

遍历字符:从左到右遍历每个字符,查找对应的值。map[str[i]] || 0 如果找不到字符(比如非法字符),返回 0。

减法规则判断

  • 获取当前字符的值 curr 和下一个字符的值 next
  • 如果 curr < next,说明是减法规则,比如 IV(4),I 在 V 前面,I 的值小于 V,所以 I 要减去
  • 否则是加法规则,直接加上当前值

示例:MMXXIV

  • M: curr=1000, next=1000, 1000 >= 1000,加 1000,result=1000
  • M: curr=1000, next=10, 1000 >= 10,加 1000,result=2000
  • X: curr=10, next=10, 10 >= 10,加 10,result=2010
  • X: curr=10, next=1, 10 >= 1,加 10,result=2020
  • I: curr=1, next=5, 1 < 5,减 1,result=2019
  • V: curr=5, next=0(没有下一个),5 >= 0,加 5,result=2024

为什么这个算法有效?因为罗马数字的减法规则只有 6 种:IV、IX、XL、XC、CD、CM。这些组合中,小数字总是在大数字前面,且只有一个小数字。算法通过比较相邻两个字符的大小,自动处理所有减法情况。

状态和动画初始化

  useEffect(() => {
    exampleAnims.forEach((anim, i) => {
      setTimeout(() => {
        Animated.spring(anim, { toValue: 1, friction: 5, useNativeDriver: true }).start();
      }, i * 60);
    });
  }, []);

组件加载时,对照表的 8 个示例依次弹出。

动画数组exampleAnims 是包含 8 个动画值的数组,每个示例对应一个动画值。Array(8).fill(0).map(() => new Animated.Value(0)) 创建 8 个初始值为 0 的动画值。

延迟启动setTimeout(() => {...}, i * 60) 让每个示例延迟 60ms 启动。第一个示例立即启动(0ms),第二个延迟 60ms,第三个延迟 120ms,以此类推。这样 8 个示例依次弹出,形成"波浪"效果。

弹簧动画Animated.spring 从 0 弹到 1,friction: 5 让弹簧有明显的回弹效果。动画值同时控制缩放和透明度,示例从无到有、从小到大地出现。

为什么用 60ms 间隔?因为 60ms 刚好是 1 帧(60fps),视觉上流畅自然。如果间隔太短(比如 20ms),动画会显得太快太乱;如果间隔太长(比如 200ms),用户要等很久才能看到所有示例。

按钮动画

  const animateButton = (anim: Animated.Value) => {
    Animated.sequence([
      Animated.timing(anim, { toValue: 0.9, duration: 100, useNativeDriver: true }),
      Animated.spring(anim, { toValue: 1, friction: 3, useNativeDriver: true }),
    ]).start();
    
    Animated.sequence([
      Animated.timing(resultAnim, { toValue: 0.95, duration: 100, useNativeDriver: true }),
      Animated.spring(resultAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
    ]).start();
  };

这个函数同时触发两个动画:按钮动画和结果卡片动画。

按钮动画:传入的 anim 参数指定要动画化的按钮。先缩小到 90%(100ms),再弹回到 100%。friction: 3 让弹簧有明显的回弹效果,给用户"按下去又弹起来"的触感反馈。

结果卡片动画resultAnim 控制两个输入卡片的缩放。卡片缩小到 95%,再弹回到 100%。friction: 4 比按钮的摩擦力大,弹性稍弱,动画更稳定。

为什么同时动画化按钮和卡片?因为点击按钮会更新输入框的内容,同时动画化按钮和卡片,给用户"点击 → 按钮反馈 → 结果更新"的完整视觉流程。

为什么卡片缩小到 95% 而按钮缩小到 90%?因为卡片比按钮大,相同的缩放比例在大元素上更明显。95% 的缩放刚刚好,既能让用户注意到变化,又不会太夸张。

转换函数

  const convertToRoman = () => {
    animateButton(buttonAnim1);
    const num = parseInt(arabic);
    if (!isNaN(num)) setRoman(toRoman(num));
  };

  const convertToArabic = () => {
    animateButton(buttonAnim2);
    const num = toArabic(roman);
    if (num > 0) setArabic(num.toString());
  };

两个转换函数分别处理阿拉伯数字转罗马数字和罗马数字转阿拉伯数字。

阿拉伯转罗马

  • 先触发按钮动画,给用户即时反馈
  • parseInt(arabic) 把输入的字符串转成整数
  • !isNaN(num) 检查是否是有效数字,如果是,调用 toRoman 转换并更新 roman 状态

罗马转阿拉伯

  • 先触发按钮动画
  • 调用 toArabic 转换罗马数字
  • num > 0 检查结果是否有效(罗马数字至少是 1),如果有效,转成字符串并更新 arabic 状态

为什么要检查有效性?因为用户可能输入非法内容。比如输入"abc",parseInt 返回 NaN,不应该更新罗马数字。输入"XYZ",toArabic 返回 0(因为 Y 和 Z 不是罗马符号),不应该更新阿拉伯数字。

对照表数据

  const examples = [
    { arabic: 1, roman: 'I' }, { arabic: 5, roman: 'V' }, { arabic: 10, roman: 'X' },
    { arabic: 50, roman: 'L' }, { arabic: 100, roman: 'C' }, { arabic: 500, roman: 'D' },
    { arabic: 1000, roman: 'M' }, { arabic: 2024, roman: 'MMXXIV' },
  ];

对照表包含 8 个示例,前 7 个是基本符号(I、V、X、L、C、D、M),最后一个是当前年份 2024。

为什么选择这 7 个基本符号?因为所有罗马数字都由这 7 个符号组成。用户看到这 7 个符号,就能理解罗马数字的基本规则。

为什么加上 2024?因为 2024 是一个实际的例子,展示了如何组合多个符号。MMXXIV = 1000 + 1000 + 10 + 10 + 4,用户可以看到贪心算法的效果。

界面渲染:头部和阿拉伯数字输入

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerIcon}>🏛️</Text>
        <Text style={styles.headerTitle}>罗马数字</Text>
        <Text style={styles.headerSubtitle}>阿拉伯数字与罗马数字互转</Text>
      </View>

      <Animated.View style={[styles.card, { transform: [{ scale: resultAnim }] }]}>
        <Text style={styles.label}>🔢 阿拉伯数字</Text>
        <TextInput
          style={styles.input}
          value={arabic}
          onChangeText={setArabic}
          keyboardType="numeric"
          placeholder="1-3999"
          placeholderTextColor="#666"
        />
        <Animated.View style={{ transform: [{ scale: buttonAnim1 }] }}>
          <TouchableOpacity style={styles.btn} onPress={convertToRoman} activeOpacity={0.8}>
            <Text style={styles.btnText}>转换为罗马数字 ⬇️</Text>
          </TouchableOpacity>
        </Animated.View>
      </Animated.View>

头部区域包含图标、标题、副标题。🏛️ 古罗马建筑图标表示"罗马"的概念,和罗马数字的历史渊源相符。

阿拉伯数字输入卡片

  • Animated.View 包裹,应用 resultAnim 动画
  • 标签"🔢 阿拉伯数字"明确告诉用户这里输入普通数字
  • keyboardType="numeric" 弹出数字键盘,方便输入
  • 占位符"1-3999"提示有效范围
  • 输入框字号 24,居中对齐,数字醒目

转换按钮

  • Animated.View 包裹,应用 buttonAnim1 动画
  • 点击时触发 convertToRoman 函数
  • ⬇️ 箭头表示"向下转换",从阿拉伯数字转到罗马数字

罗马数字输入和反向转换

      <Animated.View style={[styles.card, { transform: [{ scale: resultAnim }] }]}>
        <Text style={styles.label}>🏛️ 罗马数字</Text>
        <TextInput
          style={[styles.input, styles.romanInput]}
          value={roman}
          onChangeText={setRoman}
          placeholder="MMXXIV"
          placeholderTextColor="#666"
          autoCapitalize="characters"
        />
        <Animated.View style={{ transform: [{ scale: buttonAnim2 }] }}>
          <TouchableOpacity style={styles.btn} onPress={convertToArabic} activeOpacity={0.8}>
            <Text style={styles.btnText}>转换为阿拉伯数字 ⬆️</Text>
          </TouchableOpacity>
        </Animated.View>
      </Animated.View>

罗马数字输入卡片

  • 同样用 Animated.View 包裹,应用 resultAnim 动画
  • 标签"🏛️ 罗马数字"明确告诉用户这里输入罗马数字
  • autoCapitalize="characters" 自动转大写,因为罗马数字标准写法是大写
  • 占位符"MMXXIV"(2024 的罗马数字)提示用户格式
  • romanInput 样式应用等宽字体和字符间距,让罗马数字更易读

反向转换按钮

  • Animated.View 包裹,应用 buttonAnim2 动画
  • 点击时触发 convertToArabic 函数
  • ⬆️ 箭头表示"向上转换",从罗马数字转到阿拉伯数字

为什么用两个独立的输入框和按钮?因为用户可能在任意一个输入框输入内容,然后选择转换方向。两个输入框和两个按钮让转换方向更明确,用户不需要思考"我应该在哪里输入"。

对照表渲染

      <View style={styles.examples}>
        <Text style={styles.examplesTitle}>📚 常用对照</Text>
        {examples.map(({ arabic: a, roman: r }, i) => (
          <Animated.View
            key={a}
            style={[styles.exampleRow, {
              transform: [{ scale: exampleAnims[i] }],
              opacity: exampleAnims[i],
            }]}
          >
            <Text style={styles.exampleArabic}>{a}</Text>
            <View style={styles.exampleDivider} />
            <Text style={styles.exampleRoman}>{r}</Text>
          </Animated.View>
        ))}
      </View>
    </ScrollView>
  );
};

对照表显示 8 个常用数字和对应的罗马数字,方便用户查阅和学习。

动画渲染:每个示例行用 Animated.View 包裹,应用对应的动画值。transform: [{ scale: exampleAnims[i] }] 控制缩放,opacity: exampleAnims[i] 控制透明度。动画值从 0 到 1,示例从无到有、从小到大地出现。

示例行布局

  • 左边是阿拉伯数字,白色,居中对齐
  • 中间是蓝色分隔线,宽 30 像素,高 2 像素
  • 右边是罗马数字,蓝色粗体,居中对齐

为什么用分隔线而不是箭头?因为分隔线更简洁,不暗示转换方向。对照表是双向的,用户可以从阿拉伯数字查罗马数字,也可以从罗马数字查阿拉伯数字。

为什么示例行有底部边框?因为边框把每个示例分隔开,让表格更清晰。最后一个示例没有底部边框(CSS 默认行为),避免多余的线条。

鸿蒙 ArkTS 对比:转换算法

toRoman(num: number): string {
  if (num <= 0 || num > 3999) return '范围: 1-3999'
  const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
  const symbols = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
  let result = ''
  for (let i = 0; i < values.length; i++) {
    while (num >= values[i]) {
      result += symbols[i]
      num -= values[i]
    }
  }
  return result
}

toArabic(s: string): number {
  const map: Record<string, number> = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }
  let result = 0
  const str = s.toUpperCase()
  for (let i = 0; i < str.length; i++) {
    const curr = map[str[i]] || 0
    const next = map[str[i + 1]] || 0
    if (curr < next) result -= curr
    else result += curr
  }
  return result
}

ArkTS 中的转换算法完全一样,因为算法逻辑是纯 JavaScript,不依赖任何平台特性。贪心算法和减法规则判断在任何平台上都通用。唯一的区别是类型声明,ArkTS 用 Record<string, number> 代替 { [key: string]: number },但运行时行为完全相同。

样式定义:容器和卡片

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
  header: { alignItems: 'center', marginBottom: 24 },
  headerIcon: { fontSize: 50, marginBottom: 8 },
  headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
  headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
  card: {
    backgroundColor: '#1a1a3e',
    padding: 20,
    borderRadius: 16,
    marginBottom: 16,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  label: { color: '#888', fontSize: 14, marginBottom: 12 },

容器用深蓝黑色背景(#0f0f23),营造深色主题。卡片用稍浅的深蓝色(#1a1a3e),和背景形成对比。边框用更浅的蓝色(#3a3a6a),让卡片有立体感。

标签用灰色(#888),字号 14,起辅助说明作用。标签和输入框之间有 12 像素间距,让布局透气。

样式定义:输入框和按钮

  input: {
    backgroundColor: '#252550',
    padding: 16,
    borderRadius: 12,
    fontSize: 24,
    textAlign: 'center',
    color: '#fff',
    marginBottom: 16,
  },
  romanInput: { fontFamily: 'monospace', letterSpacing: 4 },
  btn: {
    backgroundColor: '#4A90D9',
    padding: 14,
    borderRadius: 10,
    alignItems: 'center',
  },
  btnText: { color: '#fff', fontWeight: '700' },

输入框用更深的背景色(#252550),和卡片形成对比。字号 24,居中对齐,数字醒目。圆角 12,和卡片的圆角 16 形成层次。

罗马数字输入框特殊样式

  • fontFamily: 'monospace':等宽字体让罗马数字对齐,更易读
  • letterSpacing: 4:增加字符间距,让每个字母分开,避免粘在一起。比如 III 如果没有间距,看起来像一条线;有间距后,能清楚看到 3 个 I

按钮用蓝色背景(#4A90D9),白色粗体文字,是页面的视觉焦点。圆角 10,比输入框的圆角小,让按钮更紧凑。

样式定义:对照表

  examples: {
    backgroundColor: '#1a1a3e',
    padding: 16,
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  examplesTitle: { fontSize: 16, fontWeight: '600', marginBottom: 16, textAlign: 'center', color: '#fff' },
  exampleRow: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#2a2a4e',
  },
  exampleArabic: { flex: 1, fontSize: 18, color: '#fff', textAlign: 'center' },
  exampleDivider: { width: 30, height: 2, backgroundColor: '#4A90D9' },
  exampleRoman: { flex: 1, fontSize: 18, color: '#4A90D9', fontWeight: '600', textAlign: 'center' },
});

对照表用和卡片相同的背景色和边框,保持视觉一致性。

示例行布局

  • flexDirection: 'row':水平排列
  • alignItems: 'center':垂直居中对齐
  • paddingVertical: 12:上下内边距 12,让每行有足够的点击区域
  • borderBottomWidth: 1:底部边框分隔每行

阿拉伯数字和罗马数字

  • 都用 flex: 1,平分剩余空间
  • 都用 textAlign: 'center',居中对齐
  • 阿拉伯数字用白色,罗马数字用蓝色粗体,形成视觉对比

分隔线:宽 30 像素,高 2 像素,蓝色,连接阿拉伯数字和罗马数字。分隔线不占 flex 空间,固定宽度,让布局更稳定。

小结

这个罗马数字工具展示了贪心算法和减法规则的实现。阿拉伯转罗马用贪心算法,从大到小选择符号,保证结果最短。罗马转阿拉伯用减法规则判断,比较相邻字符的大小,自动处理所有减法情况。对照表展示基本符号和实际例子,帮助用户理解罗马数字的规则。在 OpenHarmony 平台上,算法逻辑是纯 JavaScript,跨平台通用。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐